From 9c71333cfaed4c76cd8349af53989bbb14870c81 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Tue, 10 May 2022 21:35:57 -0500 Subject: [PATCH 1/6] Rebased Reverse Dependencies --- benchmarks/RevDeps.hs | 76 ++ datafiles/static/graph/graph.css | 1 + datafiles/static/graph/tmpl.js | 86 +++ datafiles/static/graph/vivagraph.js | 114 +++ datafiles/static/hackage.css | 4 + datafiles/templates/Html/graph.html.st | 299 ++++++++ datafiles/templates/Html/package-page.html.st | 14 + .../templates/Html/table-interface.html.st | 1 + exes/ReverseDepsForPackage.hs | 15 + exes/ReverseDepsPreferredForPackage.hs | 38 + hackage-server.cabal | 44 +- src/Distribution/Server/Features.hs | 14 +- src/Distribution/Server/Features/Core.hs | 2 + src/Distribution/Server/Features/Html.hs | 213 ++++-- .../Server/Features/Html/HtmlUtilities.hs | 22 + .../Server/Features/PackageList.hs | 48 +- .../Server/Features/PreferredVersions.hs | 17 + .../Server/Features/ReverseDependencies.hs | 420 ++++++----- .../Features/ReverseDependencies/State.hs | 668 +++++++----------- src/Distribution/Server/Framework/MemSize.hs | 10 + src/Distribution/Server/Pages/Reverse.hs | 356 +++++----- tests/RevDepCommon.hs | 61 ++ tests/ReverseDependenciesTest.hs | 219 ++++++ 23 files changed, 1856 insertions(+), 886 deletions(-) create mode 100644 benchmarks/RevDeps.hs create mode 100644 datafiles/static/graph/graph.css create mode 100644 datafiles/static/graph/tmpl.js create mode 100644 datafiles/static/graph/vivagraph.js create mode 100644 datafiles/templates/Html/graph.html.st create mode 100644 exes/ReverseDepsForPackage.hs create mode 100644 exes/ReverseDepsPreferredForPackage.hs create mode 100644 tests/RevDepCommon.hs create mode 100644 tests/ReverseDependenciesTest.hs diff --git a/benchmarks/RevDeps.hs b/benchmarks/RevDeps.hs new file mode 100644 index 000000000..1548d22dc --- /dev/null +++ b/benchmarks/RevDeps.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE ScopedTypeVariables , TypeApplications #-} +module Main where + +import Control.Monad (replicateM) +import Data.Containers.ListUtils (nubOrd) +import qualified Data.Vector as Vector +import Distribution.Package (packageName) +import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getDependenciesFlat) +import Distribution.Server.Packages.PackageIndex as PackageIndex + +import Gauge.Benchmark (nfAppIO, bench) +import Gauge.Main (defaultMain) +import System.Random.Stateful + +import RevDepCommon (Package(..), packToPkgInfo, TestPackage(..)) + +randomPacks + :: forall m g. StatefulGen g m + => g + -> Int + -> Vector.Vector (Package TestPackage) + -> m (Vector.Vector (Package TestPackage)) +randomPacks gen limit generated | length generated < limit = do + makeNewPack <- uniformM gen -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty + then + Package + <$> pure (TestPackage (fromIntegral @Int @Word $ Vector.length generated)) + <*> uniformRM (0, 10) gen + <*> pure mempty + else do + prevIdx <- uniformRM (0, length generated - 1) gen + let Package { name = prevName } = generated Vector.! prevIdx + (prevNamePacks, nonPrevName) = Vector.partition ((== prevName) . name) generated + depPacks <- + if mempty /= nonPrevName + then do + -- TODO this should have an expected amount of deps equal to what is actually on hackage. what is it? + numOfDeps <- uniformRM (1, min (length nonPrevName - 1) 7) gen + indicesMayDuplicate <- replicateM numOfDeps (uniformRM (0, length nonPrevName - 1) gen) + let indices = nubOrd indicesMayDuplicate + pure $ map (nonPrevName Vector.!) indices + else + pure [] + let + newVersion = + if mempty /= prevNamePacks + then 1 + maximum (fmap version prevNamePacks) + else 0 + pure $ + Package + { name = prevName + , version = newVersion + , deps = map name depPacks + } + let added = generated <> pure toInsert + randomPacks gen limit added +randomPacks _ _ generated = pure generated + +main :: IO () +main = do + packs :: Vector.Vector (Package TestPackage) <- randomPacks globalStdGen 20000 mempty + let idx = PackageIndex.fromList $ map packToPkgInfo (Vector.toList packs) + Right revs <- pure $ constructReverseIndex idx + let numPacks = length packs + defaultMain $ + (:[]) $ + bench "get transitive dependencies for one randomly selected package" $ + flip nfAppIO revs $ \revs' -> do + select <- uniformRM (0, numPacks - 1) globalStdGen + -- TODO why are there so many transitive deps? + length <$> + getDependenciesFlat + (packageName $ packToPkgInfo (packs Vector.! select)) + revs' diff --git a/datafiles/static/graph/graph.css b/datafiles/static/graph/graph.css new file mode 100644 index 000000000..4ad5cafc2 --- /dev/null +++ b/datafiles/static/graph/graph.css @@ -0,0 +1 @@ + *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */.label,sub,sup{vertical-align:baseline}.search ul,hr{box-sizing:content-box}hr,img{border:0}body,figure{margin:0}.btn-group>.btn-group,.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.dropdown-menu{float:left}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.pre-scrollable{max-height:340px}.form-control-feedback,.navigation-help,.node-hover-list,.node-hover-tooltip,.steering,a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0}mark{background:#ff0;color:#000}sub,sup{font-size:75%;line-height:0;position:relative}sup{top:-.5em}sub{bottom:-.25em}img{vertical-align:middle}svg:not(:root){overflow:hidden}hr{height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}.glyphicon,address{font-style:normal}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{blockquote,img,pre,tr{page-break-inside:avoid}*,:after,:before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999}thead{display:table-header-group}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}.btn,.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-warning.active,.btn-warning:active,.btn.active,.btn:active,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover,.form-control,.navbar-toggle,.open>.dropdown-toggle.btn-danger,.open>.dropdown-toggle.btn-default,.open>.dropdown-toggle.btn-info,.open>.dropdown-toggle.btn-primary,.open>.dropdown-toggle.btn-warning{background-image:none}@font-face{font-family:'Glyphicons Halflings';src:url(glyphicons-halflings-regular.eot);src:url(glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(glyphicons-halflings-regular.woff2) format('woff2'),url(glyphicons-halflings-regular.woff) format('woff'),url(glyphicons-halflings-regular.ttf) format('truetype'),url(glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\e227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\00a5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-size:14px;line-height:1.42857143;color:#333}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}dt,kbd kbd,label{font-weight:700}address,blockquote .small,blockquote footer,blockquote small,dd,dt,pre{line-height:1.42857143}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.list-inline,.list-unstyled{padding-left:0;list-style:none}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}pre code,table{background-color:transparent}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}dl,ol,ul{margin-top:0}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child,ol ol,ol ul,ul ol,ul ul{margin-bottom:0}address,dl{margin-bottom:20px}ol,ul{margin-bottom:10px}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.container{width:750px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;color:#777}legend,pre{display:block;color:#333}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}code,kbd{padding:2px 4px;font-size:90%}caption,th{text-align:left}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{color:#fff;background-color:#333;border-radius:3px;box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;box-shadow:none}pre{padding:9.5px;margin:0 0 10px;font-size:13px;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}.container,.container-fluid{margin-right:auto;margin-left:auto}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;border-radius:0}.container,.container-fluid{padding-left:15px;padding-right:15px}.pre-scrollable{overflow-y:scroll}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.row{margin-left:-15px;margin-right:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}caption{padding-top:8px;padding-bottom:8px;color:#777}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover,.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset,legend{padding:0;border:0}fieldset{margin:0;min-width:0}legend{width:100%;margin-bottom:20px;font-size:21px;line-height:inherit;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-appearance:none}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}.form-control,output{font-size:14px;line-height:1.42857143;color:#555;display:block}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}output{padding-top:7px}.form-control{width:100%;height:34px;padding:6px 12px;background-color:#fff;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .form-control-feedback,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio label,fieldset[disabled] .radio-inline,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:34px}.form-control-static.input-lg,.form-control-static.input-sm{padding-left:0;padding-right:0}.form-group-sm .form-control,.input-sm{padding:5px 10px;border-radius:3px;font-size:12px}.input-sm{height:30px;line-height:1.5}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;line-height:1.5}.form-group-lg .form-control,.input-lg{border-radius:6px;padding:10px 16px;font-size:18px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;line-height:1.3333333}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;line-height:1.3333333}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.collapsing,.dropdown,.dropup{position:relative}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .form-control-feedback,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .form-control-feedback,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-control-static,.form-inline .form-group{display:inline-block}.form-inline .control-label,.form-inline .form-group{margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;touch-action:manipulation;cursor:pointer;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.42857143;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:dotted thin;outline:-webkit-focus-ring-color auto 5px;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{color:#337ab7;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu-right,.dropdown-menu.pull-right{left:auto;right:0}.dropdown-header,.dropdown-menu>li>a{display:block;padding:3px 20px;line-height:1.42857143;white-space:nowrap}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle,.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child,.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child),.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn,.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{clear:both;font-weight:400;color:#333}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#337ab7}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{font-size:12px;color:#777}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.nav-justified>.dropdown .dropdown-menu,.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn .caret,.btn-group>.btn:first-child{margin-left:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn-lg .caret{border-width:5px 5px 0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-radius:4px 4px 0 0}.btn-group-vertical>.btn:last-child:not(:first-child){border-radius:0 0 4px 4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.nav>li,.nav>li>a{display:block;position:relative}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px;margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0;border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-justified>li,.nav-stacked>li{float:none}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar{border-radius:4px}.navbar-header{float:left}.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.embed-responsive,.modal,.modal-open,.progress{overflow:hidden}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}.navbar-static-top{z-index:1000;border-width:0 0 1px}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px;font-size:18px;line-height:20px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}.progress-bar-striped,.progress-striped .progress-bar,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}@media (min-width:768px){.navbar-toggle{display:none}.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);margin:8px -15px}@media (min-width:768px){.navbar-form .form-control-static,.navbar-form .form-group{display:inline-block}.navbar-form .control-label,.navbar-form .form-group{margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.breadcrumb>li,.pagination{display:inline-block}.btn .badge,.btn .label{top:-1px;position:relative}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-radius:4px 4px 0 0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#e7e7e7;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#080808;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{padding-left:0;margin:20px 0;border-radius:4px}.pager li,.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.42857143;text-decoration:none;color:#337ab7;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.close,.list-group-item>.badge,.pager .next>a,.pager .next>span{float:right}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#337ab7;border-color:#337ab7;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.badge,.label{font-weight:700;line-height:1;white-space:nowrap;text-align:center}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:20px 0;list-style:none;text-align:center}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}a.badge:focus,a.badge:hover,a.label:focus,a.label:hover{color:#fff;cursor:pointer;text-decoration:none}.label{display:inline;padding:.2em .6em .3em;font-size:75%;border-radius:.25em}.label:empty{display:none}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;color:#fff;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.media-object,.thumbnail{display:block}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.alert,.thumbnail{margin-bottom:20px}.alert .alert-link,.close{font-weight:700}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-left:auto;margin-right:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.modal,.modal-backdrop{top:0;right:0;bottom:0;left:0}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.carousel-indicators li,.in-degree,.node-focus,.out-degree,button.close{cursor:pointer}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.panel-heading>.dropdown .dropdown-toggle,.panel-title,.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-title,.panel>.list-group,.panel>.panel-collapse>.list-group,.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-title{margin-top:0;font-size:16px}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel-group .panel-heading,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-left-radius:3px;border-bottom-right-radius:3px}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{font-size:21px;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.popover,.tooltip{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.42857143;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;text-decoration:none}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;background:0 0;border:0;-webkit-appearance:none}.modal-content,.popover{background-clip:padding-box}.modal{display:none;position:fixed;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.modal-backdrop{position:fixed;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;text-align:left;text-align:start;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow,.tooltip.bottom-left .tooltip-arrow,.tooltip.bottom-right .tooltip-arrow{border-width:0 5px 5px;border-bottom-color:#000;top:0}.tooltip.bottom .tooltip-arrow{left:50%;margin-left:-5px}.tooltip.bottom-left .tooltip-arrow{right:5px;margin-top:-5px}.tooltip.bottom-right .tooltip-arrow{left:5px;margin-top:-5px}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;text-align:start;font-size:14px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.carousel-caption,.carousel-control{color:#fff;text-shadow:0 1px 2px rgba(0,0,0,.6);text-align:center}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.carousel,.carousel-inner{position:relative}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.left>.arrow:after,.popover.right>.arrow:after{content:" ";bottom:-10px}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{left:1px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;border-right-width:0;border-left-color:#fff}.carousel-inner{overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-moz-transition:-moz-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;-moz-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;background-color:rgba(0,0,0,0)}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;background-color:#000\9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px}.carousel-caption .btn,.text-hide{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{content:" ";display:table}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.hidden,.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;background-color:transparent;border:0}.affix{position:fixed}.steering,.steering .inner{position:absolute;top:50%;left:50%}@-ms-viewport{width:device-width}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}.visible-xs-block{display:block!important}.visible-xs-inline{display:inline!important}.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}.visible-sm-block{display:block!important}.visible-sm-inline{display:inline!important}.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}.visible-md-block{display:block!important}.visible-md-inline{display:inline!important}.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}.visible-lg-block{display:block!important}.visible-lg-inline{display:inline!important}.visible-lg-inline-block{display:inline-block!important}.hidden-lg{display:none!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}.hidden-print{display:none!important}}.steering{width:200px;height:200px;z-index:1;margin-left:-100px;margin-top:-100px;border-radius:200px;border:2px solid rgba(255,255,255,.6)}.steering .inner{width:100px;height:100px;margin-left:-50px;margin-top:-50px;border-radius:100px;border:2px solid rgba(255,255,255,.2)}.steering .steering-help{position:fixed;color:rgba(255,255,255,.9);bottom:12px;font-family:'PT Sans',sans-serif;font-size:large;left:50%;margin-left:-187px}#app,.graph-full-size,.label,.navigation-help,.node-details,.node-hover-list,.search,.window-container{position:absolute}.node-details h2,.node-details h4{margin:0;font-family:'PT Sans',sans-serif}.node-details{width:395px;left:16px;bottom:12px;padding:12px;z-index:1;font-family:'PT Sans',sans-serif;background:rgba(0,0,0,.85);border:1px solid grey;color:#fff}.node-details h4{display:block;display:-webkit-box;max-width:400px;height:50.4px;font-size:18px;line-height:1.4;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis}.node-details h2{text-align:center}.node-details .small{text-align:center;color:#999}@media (max-width:768px){.node-details{right:12px;width:inherit}.node-details .info-block{padding-left:0}.node-details .info-block a{display:inline-block;margin:0 7px}}.node-details .info-block a{font-size:18px}.github-avatar-detail{width:50px;margin-left:-16px;margin-right:8px;float:left}.search{top:12px;z-index:1}.search .search-results{padding-top:12px;background:rgba(0,0,0,.85)}.search .search-results h4{margin:0 0 5px;padding:0 0 5px 20px}.search .scroll-wrapper{border-top:1px solid grey;height:250px;overflow:hidden}.search ul{overflow-y:scroll;overflow-x:hidden;width:100%;height:100%;color:#fff;list-style:none;margin:0;padding:0 20px 0 0}.search ul li{height:25px}#app,.graph-full-size,.search ul li a{width:100%;height:100%}.search ul li a{opacity:.5;color:#fff;text-decoration:none;display:block;margin:0;padding:0 0 0 20px;line-height:25px;white-space:nowrap}.search ul li a:focus,.search ul li a:hover{text-decoration:none;opacity:1;background:rgba(26,26,26,.95)}.search .search-form input{width:100%;background:rgba(26,26,26,.5);color:#fff;border-radius:0;border:0}.search .search-form .input-group{border:1px solid grey}.search .search-form .input-group.focused,.search .search-form .input-group:hover{border-color:#fff}.search .search-form .input-group.focused button.btn,.search .search-form .input-group:hover button.btn{background:#66afe9}.search .search-form button.btn{color:#fff;border-radius:0;background:grey}.search h4{color:#fff}.search h4 .small{opacity:.5}body{background-color:#000;font-family:Roboto,sans-serif}#app{overflow:auto}.graph-full-size{top:0;left:0;overflow:hidden}h1,h2{color:#efefef}h4{font-family:'PT Sans',sans-serif}.window-container{background-color:rgba(0,0,0,.8);color:#7F7F7F}.window-list-content{overflow-y:auto;overflow-x:hidden;max-height:200px}.search-results-window{left:16px;top:47px;padding:10px;width:395px}.degree-results-window{left:16px;bottom:87px;padding:10px;width:395px}.no-overflow{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.window-title strong{color:#fff;font-weight:400;font-size:1.2em}.window-title .node-name{color:#fff}.label{color:#fff;background:#000;z-index:1}.loading{padding:10px;left:10px;top:48px}.about{padding:10px;right:10px;top:0}@media (max-width:768px){.degree-results-window{display:none}.about{padding:10px;right:13px;bottom:22px;top:inherit}}.node-hover-tooltip{position:absolute;background:rgba(0,0,0,.8);color:#fff;padding:4px 10px;min-width:200px;border:1px solid grey}.in-degree,.window-indegree{color:#90EE90}.node-hover-tooltip span{margin:0 5px}.out-degree,.window-outdegree{color:#F08080}.node-hover-list{background:#000;right:-20px;bottom:10px;height:150px;color:#fff;padding:4px;width:220px;overflow-y:scroll}.node-hover-list ul{padding:0}.node-hover-list li{list-style-type:none}.vcenter{display:inline-block;vertical-align:middle;float:none}a.reset-color,a.reset-color:focus,a.reset-color:hover{text-decoration:none;color:inherit}a.media:focus,a.media:hover{text-decoration:none;color:#FF008C;border:none}a.media{color:#fff;background-color:#000;border:none}.error{color:pink}.error-details{color:wheat}.navigation-help{background-color:rgba(0,0,0,.55);padding:7px;right:20px;top:50%;transform:translateY(-50%);font-family:Roboto,sans-serif}.navigation-help h3{color:#fff;border-bottom:1px solid #fff;padding-bottom:6px;margin-top:2px}.navigation-help code{font-family:'PT Sans',sans-serif;font-size:100%;color:#fff;background-color:inherit}.navigation-help code.important-key{color:#90EE90}.navigation-help td{color:#999;text-align:right}.navigation-help td:nth-child(even){text-align:left}.navigation-help tr.spacer-row{border-bottom:10px solid transparent} diff --git a/datafiles/static/graph/tmpl.js b/datafiles/static/graph/tmpl.js new file mode 100644 index 000000000..88968fd95 --- /dev/null +++ b/datafiles/static/graph/tmpl.js @@ -0,0 +1,86 @@ +/* + * JavaScript Templates + * https://github.com/blueimp/JavaScript-Templates + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + * + * Inspired by John Resig's JavaScript Micro-Templating: + * http://ejohn.org/blog/javascript-micro-templating/ + */ + +/*global document, define, module */ + +;(function ($) { + 'use strict' + var tmpl = function (str, data) { + var f = !/[^\w\-\.:]/.test(str) + ? tmpl.cache[str] = tmpl.cache[str] || tmpl(tmpl.load(str)) + : new Function(// eslint-disable-line no-new-func + tmpl.arg + ',tmpl', + 'var _e=tmpl.encode' + tmpl.helper + ",_s='" + + str.replace(tmpl.regexp, tmpl.func) + "';return _s;" + ) + return data ? f(data, tmpl) : function (data) { + return f(data, tmpl) + } + } + tmpl.cache = {} + tmpl.load = function (id) { + return document.getElementById(id).innerHTML + } + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g + tmpl.func = function (s, p1, p2, p3, p4, p5) { + if (p1) { // whitespace, quote and backspace in HTML context + return { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + ' ': ' ' + }[p1] || '\\' + p1 + } + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%} + if (p2 === '=') { + return "'+_e(" + p3 + ")+'" + } + return "'+(" + p3 + "==null?'':" + p3 + ")+'" + } + if (p4) { // evaluation start tag: {% + return "';" + } + if (p5) { // evaluation end tag: %} + return "_s+='" + } + } + tmpl.encReg = /[<>&"'\x00]/g + tmpl.encMap = { + '<': '<', + '>': '>', + '&': '&', + '"': '"', + "'": ''' + } + tmpl.encode = function (s) { + return (s == null ? '' : '' + s).replace( + tmpl.encReg, + function (c) { + return tmpl.encMap[c] || '' + } + ) + } + tmpl.arg = 'o' + tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" + + ',include=function(s,d){_s+=tmpl(s,d);}' + if (typeof define === 'function' && define.amd) { + define(function () { + return tmpl + }) + } else if (typeof module === 'object' && module.exports) { + module.exports = tmpl + } else { + $.tmpl = tmpl + } +}(this)) diff --git a/datafiles/static/graph/vivagraph.js b/datafiles/static/graph/vivagraph.js new file mode 100644 index 000000000..673125611 --- /dev/null +++ b/datafiles/static/graph/vivagraph.js @@ -0,0 +1,114 @@ +!function(S){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=S();else if("function"==typeof define&&define.amd)define([],S);else{var e;"undefined"!=typeof window?e=window:"undefined"!=typeof global?e=global:"undefined"!=typeof self&&(e=self);e.Viva=S()}}(function(){return function e(g,l,d){function m(a,c){if(!l[a]){if(!g[a]){var f="function"==typeof require&&require;if(!c&&f)return f(a,!0);if(b)return b(a,!0);f=Error("Cannot find module '"+a+"'");throw f.code="MODULE_NOT_FOUND", +f;}f=l[a]={exports:{}};g[a][0].call(f.exports,function(f){var c=g[a][1][f];return m(c?c:f)},f,f.exports,e,g,l,d)}return l[a].exports}for(var b="function"==typeof require&&require,c=0;ce?e-0:e+0)/b;e=k*r-n*a;x.y=(0>e?e-0:e+0)/b;return x}},{}],3:[function(e,g,l){g.exports.degree=e("./src/degree.js");g.exports.betweenness=e("./src/betweenness.js")},{"./src/betweenness.js":4,"./src/degree.js":5}],4:[function(e,g,l){g.exports=function(d,m){function b(a){r[a]/=2}function c(a){e[a.id]=0}function a(a){function c(a){a=a.id;-1===n[a]&&(n[a]=n[b]+1,p.push(a));n[a]===n[b]+1&&(k[a]+=k[b],h[a].push(b))}d.forEachNode(function(a){a=a.id;h[a]=[];n[a]=-1;k[a]=0}); +n[a]=0;k[a]=1;for(p.push(a);p.length;){var b=p.shift();Object.create(null);f.push(b);d.forEachLinkedNode(b,c,m)}}var p=[],f=[],h=Object.create(null),n=Object.create(null),k=Object.create(null),e=Object.create(null),g,r=Object.create(null);d.forEachNode(function(a){r[a.id]=0});d.forEachNode(function(b){g=b.id;a(g);for(d.forEachNode(c);f.length;){b=f.pop();for(var p=(1+e[b])/k[b],t=h[b],n=0;na)))return k.splice(a,1),0===k.length&&r.reset(),!0},addSpring:function(a,b,c,f,h){if(!a||!b)throw Error("Cannot add null spring to force simulator");"number"!==typeof c&&(c= +-1);a=new m(a,b,c,0<=h?h:-1,f);g.push(a);return a},removeSpring:function(a){if(a&&(a=g.indexOf(a),-1h&&(h=k.pos.x); +k.pos.yn&&(n=k.pos.y)}c.x1=b;c.x2=h;c.y1=f;c.y2=n}},reset:function(){c.x1=c.y1=0;c.x2=c.y2=0},getBestNewPosition:function(a){var p=0,f=0;if(a.length){for(var h=0;h +a.length?d.springLength:a.length,n=f.pos.x-c.pos.x,k=f.pos.y-c.pos.y,m=Math.sqrt(n*n+k*k);0===m&&(n=(b.nextDouble()-.5)/50,k=(b.nextDouble()-.5)/50,m=Math.sqrt(n*n+k*k));a=(!a.coeff||0>a.coeff?d.springCoeff:a.coeff)*(m-h)/m*a.weight;c.force.x+=a*n;c.force.y+=a*k;f.force.x-=a*n;f.force.y-=a*k}};c(d,m,["springCoeff","springLength"]);return m}},{"ngraph.expose":15,"ngraph.merge":24,"ngraph.random":25}],15:[function(e,g,l){function d(d,b,c){d.hasOwnProperty(c)&&"function"!==typeof b[c]&&(b[c]=function(a){return void 0!== +a?(d[c]=a,b):d[c]})}g.exports=function(m,b,c){if("[object Array]"===Object.prototype.toString.call(c))for(var a=0;ad&&(d=e);lk&&(k=l)}p=d-c;e=k-f;p>e?k=f+p:d=c+e;g=0;r=q();r.left=c;r.right=d;r.top=f;r.bottom=k;p=n-1;0n&&(d+=1,D=k,k=n,n+=n-D);v>l&&(d+=2,v=e,e=l,l+=l-v);(v=0===d?c.quad0:1===d?c.quad1: +2===d?c.quad2:3===d?c.quad3:null)?h.push(v,f):(v=q(),v.left=k,v.top=e,v.right=n,v.bottom=l,v.body=f,f=d,d=v,0===f?c.quad0=d:1===f?c.quad1=d:2===f?c.quad2=d:3===f&&(c.quad3=d))}},updateBodyForce:function(a){var b,c,d,h,k=0,e=0,g=1,C=0,q=1;for(f[0]=r;g;){b=f[C];var l=b.body,g=g-1,C=C+1;c=l!==a;l&&c?(c=l.pos.x-a.pos.x,d=l.pos.y-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),b=p*l.mass*a.mass/(h*h*h),k+=b*c,e+=b*d):c&&(c=b.massX/b.mass-a.pos.x, +d=b.massY/b.mass-a.pos.y,h=Math.sqrt(c*c+d*d),0===h&&(c=(m.nextDouble()-.5)/50,d=(m.nextDouble()-.5)/50,h=Math.sqrt(c*c+d*d)),(b.right-b.left)/hMath.abs(d.x-e.x)&&1E-8>b}},{}],20:[function(e,g,l){g.exports=function(){this.quad3=this.quad2=this.quad1=this.quad0=this.body=null;this.right=this.bottom=this.top=this.left=this.massY=this.massX=this.mass=0}},{}],21:[function(e,g,l){function d(b){return b}g.exports=function(b,c,a){c=c||d;a=a||d;b="string"===typeof b?JSON.parse(b):b;var p=m(),f;if(void 0===b.links||void 0===b.nodes)throw Error("Cannot load graph without links and nodes");for(f=0;fb)throw Error("Invalid number of nodes");var c=m(),a;for(a=0;ab)throw Error("At least two nodes are expected for complete graph");var c=m(),a,d;for(a=0;ab||0>c)throw Error("Graph dimensions are invalid. Number of nodes in each partition should be greater than 0");var a=m(),d,f;for(d=0;d +b)throw Error("Invalid number of nodes in balanced tree");var c=m(),a=Math.pow(2,b);0===b&&c.addNode(1);for(b=1;bb)throw Error("Invalid number of nodes");var c=m(),a;c.addNode(0);for(a=1;ab)throw Error("Invalid number of nodes");var c=d(b);c.addLink(0,b-1);c.addLink(b,2*b-1);return c},grid:function(b,c){if(1>b||1>c)throw Error("Invalid number of nodes in grid graph"); +var a=m(),d,f;if(1===b&&1===c)return a.addNode(0),a;for(d=0;db||1>c||1>a)throw Error("Invalid number of nodes in grid3 graph");var d=m(),f,h,n;if(1===b&&1===c&&1===a)return d.addNode(0),d;for(n=0;n +b)throw Error("Number of nodes shoul be >= 0");var c=m(),a;for(a=0;a=b)throw Error("Choose smaller `k`. It cannot be larger than number of nodes `n`");d=e("ngraph.random").random(d||42);var f=m(),h,n;for(h=0;hb)return!1;G();B.splice(b,1);var c=u[a.fromId],f=u[a.toId];c&&(b=d(a,c.links),0<=b&&c.links.splice(b,1));f&&(b=d(a,f.links),0<=b&&f.links.splice(b,1));M(a,"remove");L();return!0}function r(a,b){var c=u[a],d;if(!c)return null;for(d=0;d>>19)&4294967295;b=b+374761393+(b<<5)&4294967295;b=(b+3550635116^b<<9)&4294967295;b=b+4251993797+(b<<3)&4294967295;b=(b^3042594569^b>>>16)&4294967295;return(b&268435455)/268435456};return{next:function(a){return Math.floor(c()*a)},nextDouble:function(){return c()}}}g.exports= +{random:d,randomIterator:function(e,b){var c=b||d();if("function"!==typeof c.next)throw Error("customRandom does not match expected API: next() function is missing");return{forEach:function(a){var b,d,h;for(b=e.length-1;0m.indexOf(".")))throw Error("simplesvg currently does not support nested bindings");(a=d[m])?a.push(e):a=d[m]=[e]}}function b(a,b){function d(b){a.nodeValue=b[n]}var e=a.nodeValue;if(e&&(e=e.match(c))){var n=e[1];n.indexOf(".");(e=b[n])?e.push(d):e=b[n]=[d]}}g.exports=function(a){var b=Object.create(null);d(a,b);return{link:function(a){function c(b){b(a)} +Object.keys(b).forEach(function(a){b[a].forEach(c)})}}};var c=/{{(.+?)}}/},{}],30:[function(e,g,l){function d(){throw Error("DOMParser is not supported by this platform. Please open issue here https://github.com/anvaka/simplesvg");}e="undefined"===typeof DOMParser?{parseFromString:d}:new DOMParser;g.exports=e},{}],31:[function(e,g,l){function d(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.add(d,e,n,k)}function m(a,b,c,d){a.addEventListener(b,c,d)}function b(a,b,c,d){a.removeEventListener(b, +c,d)}function c(a,b,c,d){if(d)throw Error("cannot useCapture in oldIE");a.attachEvent("on"+b,c)}function a(a,b,c,d){a.detachEvent("on"+b,c)}d.removeEventListener=function(d,e,n,k){p=p||(document.addEventListener?{add:m,rm:b}:{add:c,rm:a});return p.rm(d,e,n,k)};d.addEventListener=d;g.exports=d;var p=null},{}],32:[function(e,g,l){function d(a){a=c.betweenness(a);return b(a)}function m(a,d){var f=c.degree(a,d);return b(f)}function b(a){return Object.keys(a).sort(function(b,c){return a[c]-a[b]}).map(function(b){return{key:b, +value:a[b]}})}var c=e("ngraph.centrality");g.exports=function(){return{betweennessCentrality:d,degreeCentrality:m}}},{"ngraph.centrality":3}],33:[function(e,g,l){g.exports=function(){return{density:function(d,e){var b=d.getNodesCount();return 0===b?NaN:e?d.getLinksCount()/(b*(b-1)):2*d.getLinksCount()/(b*(b-1))}}}},{}],34:[function(e,g,l){g.exports=function(e,b){var c={};return{bindDragNDrop:function(a,e){var f;if(e){f=b.getNodeUI(a.id);f=d(f);if("function"===typeof e.onStart)f.onStart(e.onStart); +if("function"===typeof e.onDrag)f.onDrag(e.onDrag);if("function"===typeof e.onStop)f.onStop(e.onStop);c[a.id]=f}else if(f=c[a.id])f.release(),delete c[a.id]}}};var d=e("./dragndrop.js")},{"./dragndrop.js":35}],35:[function(e,g,l){g.exports=function(c){var a,e,f,h,n,k,g=0,q=0,l,x=!1,y=0,t=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},z=function(a){t(a);return!1},A=function(a){a=a||window.event;var b=a.clientX,c=a.clientY;e&&e(a,{x:b-g,y:c-q});g=b;q=c},u=function(b){b=b||window.event; +if(x)return t(b),!1;if(1===b.button&&null!==window.event||0===b.button)return g=b.clientX,q=b.clientY,l=b.target||b.srcElement,a&&a(b,{x:g,y:q}),d.on("mousemove",A),d.on("mouseup",B),t(b),n=window.document.onselectstart,k=window.document.ondragstart,window.document.onselectstart=z,l.ondragstart=z,!1},B=function(a){a=a||window.event;d.off("mousemove",A);d.off("mouseup",B);window.document.onselectstart=n;l.ondragstart=k;l=null;f&&f(a)},F=function(a){if("function"===typeof h){a=a||window.event;a.preventDefault&& +a.preventDefault();a.returnValue=!1;var d,f=0,e=0;d=a||window.event;if(d.pageX||d.pageY)f=d.pageX,e=d.pageY;else if(d.clientX||d.clientY)f=d.clientX+window.document.body.scrollLeft+window.document.documentElement.scrollLeft,e=d.clientY+window.document.body.scrollTop+window.document.documentElement.scrollTop;d=[f,e];f=b(c);h(a,a.wheelDelta?a.wheelDelta/360:a.detail/-9,{x:d[0]-f[0],y:d[1]-f[1]})}},H=function(a){!h&&a?"webkit"===m.browser?c.addEventListener("mousewheel",F,!1):c.addEventListener("DOMMouseScroll", +F,!1):h&&!a&&("webkit"===m.browser?c.removeEventListener("mousewheel",F,!1):c.removeEventListener("DOMMouseScroll",F,!1));h=a},D=function(a,b){return(a.clientX-b.clientX)*(a.clientX-b.clientX)+(a.clientY-b.clientY)*(a.clientY-b.clientY)},v=function(a){if(1===a.touches.length){t(a);var b=a.touches[0],c=b.clientX,b=b.clientY;e&&e(a,{x:c-g,y:b-q});g=c;q=b}else 2===a.touches.length&&(c=D(a.touches[0],a.touches[1]),b=0,cy&&(b=1),h(a,b,{x:a.touches[0].clientX,y:a.touches[0].clientY}),y=c,t(a), +a.preventDefault&&a.preventDefault())},w=function(a){x=!1;d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);l=null;f&&f(a)},E=function(b){if(1===b.touches.length){var c=b.touches[0];t(b);b.preventDefault&&b.preventDefault();g=c.clientX;q=c.clientY;l=b.target||b.srcElement;a&&a(b,{x:g,y:q});x||(x=!0,d.on("touchmove",v),d.on("touchend",w),d.on("touchcancel",w))}else 2===b.touches.length&&(t(b),b.preventDefault&&b.preventDefault(),y=D(b.touches[0],b.touches[1]))};c.addEventListener("mousedown", +u);c.addEventListener("touchstart",E);return{onStart:function(b){a=b;return this},onDrag:function(a){e=a;return this},onStop:function(a){f=a;return this},onScroll:function(a){H(a);return this},release:function(){c.removeEventListener("mousedown",u);c.removeEventListener("touchstart",E);d.off("mousemove",A);d.off("mouseup",B);d.off("touchmove",v);d.off("touchend",w);d.off("touchcancel",w);H(null)}}};var d=e("../Utils/documentEvents.js"),m=e("../Utils/browserInfo.js"),b=e("../Utils/findElementPosition.js")}, +{"../Utils/browserInfo.js":39,"../Utils/documentEvents.js":40,"../Utils/findElementPosition.js":41}],36:[function(e,g,l){g.exports=function(e,b){var c=d(b),a=null,g={},f={x:0,y:0};c.mouseDown(function(b,d){a=b;f.x=d.clientX;f.y=d.clientY;c.mouseCapture(a);var e=g[b.id];if(e&&e.onStart)e.onStart(d,f);return!0}).mouseUp(function(b){c.releaseMouseCapture(a);a=null;if((b=g[b.id])&&b.onStop)b.onStop();return!0}).mouseMove(function(b,c){if(a){var d=g[a.id];if(d&&d.onDrag)d.onDrag(c,{x:c.clientX-f.x,y:c.clientY- +f.y});f.x=c.clientX;f.y=c.clientY;return!0}});return{bindDragNDrop:function(a,b){(g[a.id]=b)||delete g[a.id]}}};var d=e("../WebGL/webglInputEvents.js")},{"../WebGL/webglInputEvents.js":57}],37:[function(e,g,l){g.exports=function(c,a){a=d(a,{maxX:1024,maxY:1024,seed:"Deterministic randomness made me do this"});var e=m(a.seed),f=new b(Number.MAX_VALUE,Number.MAX_VALUE,Number.MIN_VALUE,Number.MIN_VALUE),h={},g=function(b){return{x:e.next(a.maxX),y:e.next(a.maxY)}},k="function"===typeof Object.create? +Object.create(null):{},l=function(a){k[a.id]=g(a);a=k[a.id];a.xf.x2&&(f.x2=a.x);a.yf.y2&&(f.y2=a.y)},q=function(){0!==c.getNodesCount()&&(f.x1=Number.MAX_VALUE,f.y1=Number.MAX_VALUE,f.x2=Number.MIN_VALUE,f.y2=Number.MIN_VALUE,c.forEachNode(l))},r=function(a){h[a.id]=a},x=function(a){for(var b=0;be.indexOf("compatible")&&m.exec(e)||[];e={browser:e[1]||"",version:e[2]||"0"}}else e={browser:"",version:"0"};g.exports=e},{}],40:[function(e,g,l){e("./nullEvents.js");g.exports={on:function(d,e){document.addEventListener(d,e)},off:function(d,e){document.removeEventListener(d, +e)}}},{"./nullEvents.js":44}],41:[function(e,g,l){g.exports=function(d){var e=0,b=0;if(d.offsetParent){do e+=d.offsetLeft,b+=d.offsetTop;while(null!==(d=d.offsetParent))}return[e,b]}},{}],42:[function(e,g,l){g.exports=function(d){if(!d)throw{message:"Cannot get dimensions of undefined container"};return{left:0,top:0,width:d.clientWidth,height:d.clientHeight}}},{}],43:[function(e,g,l){var d=e("gintersect");g.exports=function(e,b,c,a,g,f,h,n){return d(e,b,e,a,g,f,h,n)||d(e,a,c,a,g,f,h,n)||d(c,a,c,b, +g,f,h,n)||d(c,b,e,b,g,f,h,n)}},{gintersect:2}],44:[function(e,g,l){function d(){}g.exports={on:d,off:d,stop:d}},{}],45:[function(e,g,l){g.exports=function(d,e,b,c){this.x1=d||0;this.y1=e||0;this.x2=b||0;this.y2=c||0}},{}],46:[function(e,g,l){(function(d){function e(){}g.exports=function(){function b(b){var c=(new Date).getTime(),d=Math.max(0,16-(c-a)),e=h.setTimeout(function(){b(c+d)},d);a=c+d;return e}function c(a){h.clearTimeout(a)}var a=0,g=["ms","moz","webkit","o"],f,h;h="undefined"!==typeof window? +window:"undefined"!==typeof d?d:{setTimeout:e,clearTimeout:e};for(f=0;fb,c)}));e.forEachNode(A);e.off("changed",u);e.on("changed",u)}function D(){L=!1;e.off("changed",u);N&&(N.release(),N=null);c.off("resize",B);R.off();G.stop();e.forEachLink(function(a){k.renderLinks&&w.releaseLink(a)});e.forEachNode(function(a){I.bindDragNDrop(a,null);w.releaseNode(a)});v.dispose();w.release(E)}k=k||{};var v=k.layout,w=k.graphics,E=k.container,M=void 0!==k.interactive?k.interactive:!0,I,G,L= +!1,K=!0,O=!1,P=!1,Q=!1,J={offsetX:0,offsetY:0,scale:1},R=d({}),N;return{run:function(c){if(!L){E=E||window.document.body;v=v||m(e,{springLength:80,springCoeff:2E-4});w=w||b(e,{container:E});k.hasOwnProperty("renderLinks")||(k.renderLinks=!0);k.prerender=k.prerender||0;I=(w.inputManager||a)(e,w);if("number"===typeof k.prerender&&0a.id&&(a=a.id,c=B[b],B[b]=B[a],B[b].id=b,B[a]=c,B[a].id=a)},graphCenterChanged:function(a,b){t[12]=2*a/q-1;t[13]=1-2*b/r;I()},addLink:function(a,b){var c=y++,d=M(a);d.id=c;d.pos=b;v.createLink(d);B[c]=d;return D[a.id]=d},addNode:function(a,b){var c=x++,d=E(a);d.id=c;d.position=b;d.node=a;w.createNode(d);u[c]=d;return H[a.id]=d},translateRel:function(a,b){t[12]+=2*t[0]*a/q/t[0];t[13]-=2*t[5]*b/r/t[5];I()},scale:function(a,b){var c=2*b.x/q-1,d=1-2*b.y/r,c=c- +t[12],d=d-t[13];t[12]+=c*(1-a);t[13]+=d*(1-a);t[0]*=a;t[5]*=a;I();this.fire("rescaled");return t[0]},resetScale:function(){G();l&&(L(),I());return this},init:function(a){var b={};e.preserveDrawingBuffer&&(b.preserveDrawingBuffer=!0);g=a;L();G();g.appendChild(k);l=k.getContext("experimental-webgl",b);if(!l)throw window.alert("Could not initialize WebGL. Seems like the browser doesn't support it."),"Could not initialize WebGL. Seems like the browser doesn't support it.";e.enableBlending&&(l.blendFunc(l.SRC_ALPHA, +l.ONE_MINUS_SRC_ALPHA),l.enable(l.BLEND));e.clearColor&&(a=e.clearColorValue,l.clearColor(a.r,a.g,a.b,a.a),this.beginRender=function(){l.clear(l.COLOR_BUFFER_BIT)});v.load(l);v.updateSize(q/2,r/2);w.load(l);w.updateSize(q/2,r/2);I();"function"===typeof F&&F(k)},release:function(a){k&&a&&a.removeChild(k)},isSupported:function(){var a=window.document.createElement("canvas");return a&&a.getContext&&a.getContext("experimental-webgl")},releaseLink:function(a){0a.length?(b=new Float32Array(a.length*c*2),b.set(a),b):a},copyArrayPart:d,swapArrayPart:m,getLocations:function(a,c){for(var d= +{},e=0;e= +q.length){var l=new d(g*f);q.push(l)}l=q[c.textureNumber];l.ctx.drawImage(k,c.col*f,c.row*f,f,f);r[m]=k.src;n[k.src]=h;l.isDirty=!0;e(h)};k.src=c}}};return x}},{"./texture.js":52}],55:[function(e,g,l){g.exports=function(d,e){return{_texture:0,_offset:0,size:"number"===typeof d?d:32,src:e}}},{}],56:[function(e,g,l){var d=e("./webglAtlas.js"),m=e("./webgl.js");g.exports=function(){var b,c,a,e,f,h,g=0,k=new Float32Array(64),l,q,r,x;return{load:function(g){a=g;f=m(g);b=new d(1024);c=f.createProgram("attribute vec2 a_vertexPos;\nattribute float a_customAttributes;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nuniform float u_tilesPerTexture;\nvarying vec3 vTextureCoord;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0, 1);\nfloat corner = mod(a_customAttributes, 4.);\nfloat tileIndex = mod(floor(a_customAttributes / 4.), u_tilesPerTexture);\nfloat tilesPerRow = sqrt(u_tilesPerTexture);\nfloat tileSize = 1./tilesPerRow;\nfloat tileColumn = mod(tileIndex, tilesPerRow);\nfloat tileRow = floor(tileIndex/tilesPerRow);\nif(corner == 0.0) {\n vTextureCoord.xy = vec2(0, 1);\n} else if(corner == 1.0) {\n vTextureCoord.xy = vec2(1, 1);\n} else if(corner == 2.0) {\n vTextureCoord.xy = vec2(0, 0);\n} else {\n vTextureCoord.xy = vec2(1, 0);\n}\nvTextureCoord *= tileSize;\nvTextureCoord.x += tileColumn * tileSize;\nvTextureCoord.y += tileRow * tileSize;\nvTextureCoord.z = floor(floor(a_customAttributes / 4.)/u_tilesPerTexture);\n}", +"precision mediump float;\nvarying vec4 color;\nvarying vec3 vTextureCoord;\nuniform sampler2D u_sampler0;\nuniform sampler2D u_sampler1;\nuniform sampler2D u_sampler2;\nuniform sampler2D u_sampler3;\nvoid main(void) {\n if (vTextureCoord.z == 0.) {\n gl_FragColor = texture2D(u_sampler0, vTextureCoord.xy);\n } else if (vTextureCoord.z == 1.) {\n gl_FragColor = texture2D(u_sampler1, vTextureCoord.xy);\n } else if (vTextureCoord.z == 2.) {\n gl_FragColor = texture2D(u_sampler2, vTextureCoord.xy);\n } else if (vTextureCoord.z == 3.) {\n gl_FragColor = texture2D(u_sampler3, vTextureCoord.xy);\n } else { gl_FragColor = vec4(0, 1, 0, 1); }\n}"); +a.useProgram(c);h=f.getLocations(c,"a_vertexPos a_customAttributes u_screenSize u_transform u_sampler0 u_sampler1 u_sampler2 u_sampler3 u_tilesPerTexture".split(" "));a.uniform1f(h.tilesPerTexture,1024);a.enableVertexAttribArray(h.vertexPos);a.enableVertexAttribArray(h.customAttributes);e=a.createBuffer()},position:function(a,b){var c=18*a.id;k[c]=b.x-a.size;k[c+1]=b.y-a.size;k[c+2]=4*a._offset;k[c+3]=b.x+a.size;k[c+4]=b.y-a.size;k[c+5]=4*a._offset+1;k[c+6]=b.x-a.size;k[c+7]=b.y+a.size;k[c+8]=4*a._offset+ +2;k[c+9]=b.x-a.size;k[c+10]=b.y+a.size;k[c+11]=4*a._offset+2;k[c+12]=b.x+a.size;k[c+13]=b.y-a.size;k[c+14]=4*a._offset+1;k[c+15]=b.x+a.size;k[c+16]=b.y+a.size;k[c+17]=4*a._offset+3},createNode:function(a){k=f.extendArray(k,g,18);g+=1;var c=b.getCoordinates(a.src);c?a._offset=c.offset:(a._offset=0,b.load(a.src,function(b){a._offset=b.offset}))},removeNode:function(a){0d-H&&f[0]===z?g(x,f):g(r,f),H=d,g(C,f)&&c(a))})})(e.getGraphicsRoot());var z={mouseEnter:function(a){"function"=== +typeof a&&h.push(a);return z},mouseLeave:function(a){"function"===typeof a&&l.push(a);return z},mouseDown:function(a){"function"===typeof a&&k.push(a);return z},mouseUp:function(a){"function"===typeof a&&C.push(a);return z},mouseMove:function(a){"function"===typeof a&&q.push(a);return z},click:function(a){"function"===typeof a&&r.push(a);return z},dblClick:function(a){"function"===typeof a&&x.push(a);return z},mouseCapture:function(a){f=a},releaseMouseCapture:function(){f=null}};return e.webglInputEvents= +z}},{"../Utils/documentEvents.js":40}],58:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e){return{color:d(e)}}},{"./parseColor.js":51}],59:[function(e,g,l){var d=e("./webgl.js");g.exports=function(){var e=2*(2*Float32Array.BYTES_PER_ELEMENT+Uint32Array.BYTES_PER_ELEMENT),b,c,a,g,f,h=0,l,k=new ArrayBuffer(16*e),C=new Float32Array(k),q=new Uint32Array(k),r,x,y,t;return{load:function(e){c=e;g=d(e);b=g.createProgram("attribute vec2 a_vertexPos;\nattribute vec4 a_color;\nuniform vec2 u_screenSize;\nuniform mat4 u_transform;\nvarying vec4 color;\nvoid main(void) {\n gl_Position = u_transform * vec4(a_vertexPos/u_screenSize, 0.0, 1.0);\n color = a_color.abgr;\n}", +"precision mediump float;\nvarying vec4 color;\nvoid main(void) {\n gl_FragColor = color;\n}");c.useProgram(b);f=g.getLocations(b,["a_vertexPos","a_color","u_screenSize","u_transform"]);c.enableVertexAttribArray(f.vertexPos);c.enableVertexAttribArray(f.color);a=c.createBuffer()},position:function(a,b,c){var d=6*a.id;C[d]=b.x;C[d+1]=b.y;q[d+2]=a.color;C[d+3]=c.x;C[d+4]=c.y;q[d+5]=a.color},createLink:function(a){if((h+1)*e>k.byteLength){var b=new ArrayBuffer(2*k.byteLength),c=new Float32Array(b), +d=new Uint32Array(b);d.set(q);C=c;q=d;k=b}h+=1;l=a.id},removeLink:function(a){0a.id&&g.swapArrayPart(C,6*a.id,6*l,6);0=h.byteLength){var a=new ArrayBuffer(2*h.byteLength),b=new Float32Array(a),c=new Uint32Array(a);c.set(k);l=b;k=c;h=a}C+=1},replaceProperties:function(){},render:function(){c.useProgram(b);c.bindBuffer(c.ARRAY_BUFFER,a);c.bufferData(c.ARRAY_BUFFER,h,c.DYNAMIC_DRAW);y&&(y=!1,c.uniformMatrix4fv(g.transform,!1,x),c.uniform2f(g.screenSize,q,r));c.vertexAttribPointer(g.vertexPos,3,c.FLOAT,!1,4*Float32Array.BYTES_PER_ELEMENT,0);c.vertexAttribPointer(g.color, +4,c.UNSIGNED_BYTE,!0,4*Float32Array.BYTES_PER_ELEMENT,12);c.drawArrays(c.POINTS,0,C)}}}},{"./webgl.js":53}],61:[function(e,g,l){var d=e("./parseColor.js");g.exports=function(e,b){return{size:"number"===typeof e?e:10,color:d(b)}}},{"./parseColor.js":51}],62:[function(e,g,l){g.exports="0.8.1"},{}]},{},[1])(1)}); diff --git a/datafiles/static/hackage.css b/datafiles/static/hackage.css index 8d5f1dc41..112d57596 100644 --- a/datafiles/static/hackage.css +++ b/datafiles/static/hackage.css @@ -991,6 +991,10 @@ table.fancy th, table.properties th { padding: 0.15em 0.45em; } +table.fancy tr.even td { + background-color: #eee; +} + table.dataTable.compact.fancy tbody th, table.dataTable.compact.fancy tbody td { padding: 6px 10px; diff --git a/datafiles/templates/Html/graph.html.st b/datafiles/templates/Html/graph.html.st new file mode 100644 index 000000000..205d1680c --- /dev/null +++ b/datafiles/templates/Html/graph.html.st @@ -0,0 +1,299 @@ + + + + + Hackage Dependencies Graph + + ` + + + + + + +
+
+ +
+ +
+
+
+
+ + + + + + + + + + + diff --git a/datafiles/templates/Html/package-page.html.st b/datafiles/templates/Html/package-page.html.st index bacafe9fc..a5de893d0 100644 --- a/datafiles/templates/Html/package-page.html.st +++ b/datafiles/templates/Html/package-page.html.st @@ -213,6 +213,20 @@ $endif$ + $if(hasrdeps)$ + + ReverseDependencies + $rdeps$ + + + $endif$ + $if(hasexecs)$ Executables diff --git a/datafiles/templates/Html/table-interface.html.st b/datafiles/templates/Html/table-interface.html.st index 7eeada219..f2d834faa 100644 --- a/datafiles/templates/Html/table-interface.html.st +++ b/datafiles/templates/Html/table-interface.html.st @@ -24,6 +24,7 @@
Name
DLs
Rating
+
Rev Deps
Description
Tags
Last U/L
diff --git a/exes/ReverseDepsForPackage.hs b/exes/ReverseDepsForPackage.hs new file mode 100644 index 000000000..004a3294e --- /dev/null +++ b/exes/ReverseDepsForPackage.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE OverloadedStrings #-} +module Main where + +import Distribution.Server.Features.Core (packagesStateComponent) +import Distribution.Server.Features.Core.State (packageIndex) +import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getReverseCount) +import Distribution.Server.Framework.Feature (getState) + +main :: IO () +main = do + component <- packagesStateComponent maxBound False "state" + st <- getState component + let idx = packageIndex st + revIdx <- constructReverseIndex idx + print =<< getReverseCount "ADPfusion" revIdx diff --git a/exes/ReverseDepsPreferredForPackage.hs b/exes/ReverseDepsPreferredForPackage.hs new file mode 100644 index 000000000..7c9ec7d59 --- /dev/null +++ b/exes/ReverseDepsPreferredForPackage.hs @@ -0,0 +1,38 @@ +{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} +module Main where + +import System.Exit (die) +import Data.Maybe (isJust) +import Data.List (find) +import Control.Monad (when) +import Data.Map (toList) +import Distribution.Package +import Distribution.Version +import Distribution.Server.Features.Core (packagesStateComponent) +import Distribution.Server.Features.Core.State (packageIndex) +import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getDisplayInfo, perPackageReverse, dependsOnPkg) +import Distribution.Server.Features.PreferredVersions +import Distribution.Server.Features.PreferredVersions.State +import Distribution.Server.Framework.Feature (getState, queryState) + +main :: IO () +main = do + component <- packagesStateComponent maxBound False "state" + st <- getState component + let idx = packageIndex st + revIdx <- constructReverseIndex idx + + prefState <- preferredStateComponent False "state" + prefVersions <- queryState prefState GetPreferredVersions + let indexFunc = getDisplayInfo prefVersions idx + print $ indexFunc "ghc-prim" + print $ indexFunc "containers" + revDeps <- perPackageReverse indexFunc idx revIdx "ghc-prim" + -- Newer versions of 'containers' stopped depending on ghc-prim. So we expect 'containers' to not show up here. + print (toList revDeps) + print (dependsOnPkg idx (PackageIdentifier "ghc-prim" (mkVersion [0,6,0,1])) "containers") + let found = isJust $ find (== ("containers", (mkVersion [0,6,0,1], Nothing))) (toList revDeps) + when (not found) $ do + print revDeps + die "error!" + diff --git a/hackage-server.cabal b/hackage-server.cabal index d56c46c79..7a00fc78e 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -228,7 +228,7 @@ library lib-server Distribution.Server.Pages.Package.HaddockParse Distribution.Server.Pages.Recent Distribution.Server.Pages.AdminLog - -- [reverse index disabled] Distribution.Server.Pages.Reverse + Distribution.Server.Pages.Reverse Distribution.Server.Pages.Template Distribution.Server.Pages.Util @@ -344,8 +344,8 @@ library lib-server Distribution.Server.Features.PreferredVersions Distribution.Server.Features.PreferredVersions.State Distribution.Server.Features.PreferredVersions.Backup - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies - -- [reverse index disabled] Distribution.Server.Features.ReverseDependencies.State + Distribution.Server.Features.ReverseDependencies + Distribution.Server.Features.ReverseDependencies.State Distribution.Server.Features.Tags Distribution.Server.Features.Tags.Backup Distribution.Server.Features.Tags.State @@ -378,6 +378,7 @@ library lib-server , base16-bytestring ^>= 1.0 -- requires bumping http-io-streams , base64-bytestring ^>= 1.2.1.0 + , bimap ^>= 0.3 --NOTE: blaze-builder-0.4 is now a compat package that uses bytestring-0.10 builder , blaze-builder ^>= 0.4 , blaze-html ^>= 0.9 @@ -390,6 +391,7 @@ library lib-server , cryptohash-sha256 ^>= 0.11.100 , csv ^>= 0.1 , ed25519 ^>= 0.0.5 + , exceptions , hackage-security ^>= 0.6 , hackage-security-HTTP ^>= 0.1.1 , haddock-library > 1.7 && < 2 @@ -526,6 +528,32 @@ test-suite HighLevelTest , io-streams ^>= 1.5.0.1 , http-io-streams ^>= 0.1.6.1 +test-suite ReverseDependenciesTest + import: test-defaults + type: exitcode-stdio-1.0 + main-is: ReverseDependenciesTest.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , tasty ^>= 1.4 + , tasty-hunit ^>= 0.10 + , HUnit ^>= 1.6 + , hedgehog ^>= 1.1 + , exceptions + , bimap + other-modules: RevDepCommon + +benchmark RevDeps + import: test-defaults + type: exitcode-stdio-1.0 + hs-source-dirs: tests, benchmarks + main-is: RevDeps.hs + build-tool-depends: hackage-server:hackage-server + build-depends: + , random ^>= 1.2 + , gauge + ghc-options: -with-rtsopts=-s + other-modules: RevDepCommon + test-suite PaginationTest import: test-defaults type: exitcode-stdio-1.0 @@ -602,3 +630,13 @@ test-suite DocTests , lib-server , doctest-parallel ^>= 0.2.2 -- doctest-parallel-0.2.2 is the first to filter out autogen-modules + +executable ReverseDepsForPackage + import: exe-defaults + + main-is: ReverseDepsForPackage.hs + +executable ReverseDepsPreferredForPackage + import: exe-defaults + + main-is: ReverseDepsPreferredForPackage.hs diff --git a/src/Distribution/Server/Features.hs b/src/Distribution/Server/Features.hs index f8a8e362e..c57f8dbdb 100644 --- a/src/Distribution/Server/Features.hs +++ b/src/Distribution/Server/Features.hs @@ -33,7 +33,7 @@ import Distribution.Server.Features.BuildReports (initBuildReportsFeature import Distribution.Server.Features.PackageInfoJSON (initPackageInfoJSONFeature) import Distribution.Server.Features.LegacyRedirects (legacyRedirectsFeature) import Distribution.Server.Features.PreferredVersions (initVersionsFeature) --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies (initReverseFeature) +import Distribution.Server.Features.ReverseDependencies (initReverseFeature) import Distribution.Server.Features.DownloadCount (initDownloadFeature) import Distribution.Server.Features.Tags (initTagsFeature) import Distribution.Server.Features.AnalyticsPixels (initAnalyticsPixelsFeature) @@ -132,8 +132,8 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do initAnalyticsPixelsFeature env mkVersionsFeature <- logStartup "versions" $ initVersionsFeature env - -- mkReverseFeature <- logStartup "reverse deps" $ - -- initReverseFeature env + mkReverseFeature <- logStartup "reverse deps" $ + initReverseFeature env mkListFeature <- logStartup "list" $ initListFeature env mkSearchFeature <- logStartup "search" $ @@ -270,15 +270,13 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do tagsFeature usersFeature - {- [reverse index disabled] reverseFeature <- mkReverseFeature coreFeature versionsFeature - -} listFeature <- mkListFeature coreFeature - -- [reverse index disabled] reverseFeature + reverseFeature downloadFeature votesFeature tagsFeature @@ -299,7 +297,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do uploadFeature candidatesFeature versionsFeature - -- [reverse index disabled] reverseFeature + reverseFeature tagsFeature analyticsPixelsFeature downloadFeature @@ -384,7 +382,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do , getFeatureInterface tagsFeature , getFeatureInterface analyticsPixelsFeature , getFeatureInterface versionsFeature - -- [reverse index disabled] , getFeatureInterface reverseFeature + , getFeatureInterface reverseFeature , getFeatureInterface searchFeature , getFeatureInterface listFeature , getFeatureInterface platformFeature diff --git a/src/Distribution/Server/Features/Core.hs b/src/Distribution/Server/Features/Core.hs index c6d8835d6..6aec793ab 100644 --- a/src/Distribution/Server/Features/Core.hs +++ b/src/Distribution/Server/Features/Core.hs @@ -18,6 +18,8 @@ module Distribution.Server.Features.Core ( -- * Misc other utils packageExists, packageIdExists, + + packagesStateComponent, ) where -- stdlib diff --git a/src/Distribution/Server/Features/Html.hs b/src/Distribution/Server/Features/Html.hs index 7155f62e4..8081d256e 100644 --- a/src/Distribution/Server/Features/Html.hs +++ b/src/Distribution/Server/Features/Html.hs @@ -23,7 +23,7 @@ import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Votes import Distribution.Server.Features.Search import Distribution.Server.Features.PreferredVersions --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.PackageContents (PackageContentsFeature(..)) import Distribution.Server.Features.PackageList import Distribution.Server.Features.Tags @@ -51,7 +51,7 @@ import qualified Distribution.Server.Pages.PackageFromTemplate as PagesNew import Distribution.Server.Pages.Template import Distribution.Server.Pages.Util import qualified Distribution.Server.Pages.Group as Pages --- [reverse index disabled] import qualified Distribution.Server.Pages.Reverse as Pages +import Distribution.Server.Pages.Reverse (LatestOrOld(..), ReverseHtmlUtil(..), reverseHtmlUtil) import qualified Distribution.Server.Pages.Index as Pages import Distribution.Server.Util.CountingMap (cmFind, cmToList) import Distribution.Server.Util.DocMeta (loadTarDocMeta) @@ -102,7 +102,7 @@ initHtmlFeature :: ServerEnv -> PackageContentsFeature -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -136,13 +136,14 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, , "noscript-search-form.html" , "analytics-pixels-page.html" , "user-analytics-pixels-page.html" + , "graph.html" ] return $ \user core@CoreFeature{packageChangeHook} packages upload candidates versions - -- [reverse index disabled] reverse + reversef tags analyticsPixels download rank list@ListFeature{itemUpdate} @@ -157,6 +158,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, htmlFeature env user core packages upload candidates versions + reversef tags analyticsPixels download rank list names @@ -166,6 +168,7 @@ initHtmlFeature env@ServerEnv{serverTemplatesDir, serverTemplatesMode, reportsCore usersdetails (htmlUtilities core candidates tags user) + (reverseHtmlUtil reversef) mainCache namesCache templates @@ -201,6 +204,7 @@ htmlFeature :: ServerEnv -> UploadFeature -> PackageCandidatesFeature -> VersionsFeature + -> ReverseFeature -> TagsFeature -> AnalyticsPixelsFeature -> DownloadFeature @@ -215,6 +219,7 @@ htmlFeature :: ServerEnv -> ReportsFeature -> UserDetailsFeature -> HtmlUtilities + -> ReverseHtmlUtil -> AsyncCache Response -> AsyncCache Response -> Templates @@ -225,7 +230,7 @@ htmlFeature env@ServerEnv{..} core@CoreFeature{queryGetPackageIndex} packages upload candidates versions - -- [reverse index disabled] ReverseFeature{..} + revf@ReverseFeature{..} tags analyticsPixels download rank list@ListFeature{getAllLists} @@ -236,6 +241,7 @@ htmlFeature env@ServerEnv{..} reportsCore usersdetails utilities@HtmlUtilities{..} + reverseH@ReverseHtmlUtil{..} cachePackagesPage cacheNamesPage templates = (HtmlFeature{..}, packageIndex, packagesPage) @@ -273,6 +279,8 @@ htmlFeature env@ServerEnv{..} distros packages htmlTags + htmlReverse + revf htmlPreferred cachePackagesPage cacheNamesPage @@ -289,6 +297,7 @@ htmlFeature env@ServerEnv{..} candidates user templates htmlPreferred = mkHtmlPreferred utilities core versions htmlTags = mkHtmlTags utilities core upload user list tags templates + htmlReverse = mkHtmlReverse utilities core versions list revf reverseH htmlAnalyticsPixels = mkHtmlAnalyticsPixels utilities core user upload analyticsPixels templates @@ -303,6 +312,7 @@ htmlFeature env@ServerEnv{..} , htmlDownloadsResources htmlDownloads , htmlTagsResources htmlTags , htmlAnalyticsPixelsResources htmlAnalyticsPixels + , htmlReverseResource htmlReverse -- and user groups. package maintainers, trustees, admins , htmlGroupResource user (maintainersGroupResource . uploadResource $ upload) , htmlGroupResource user (trusteesGroupResource . uploadResource $ upload) @@ -338,74 +348,6 @@ htmlFeature env@ServerEnv{..} } -} - - -- reverse index (disabled) - {- - , (extendResource $ reversePackage reverses) { - resourceGet = [("html", serveReverse True)] - } - , (extendResource $ reversePackageOld reverses) { - resourceGet = [("html", serveReverse False)] - } - , (extendResource $ reversePackageAll reverses) { - resourceGet = [("html", serveReverseFlat)] - } - , (extendResource $ reversePackageStats reverses) { - resourceGet = [("html", serveReverseStats)] - } - , (extendResource $ reversePackages reverses) { - resourceGet = [("html", serveReverseList)] - } - -} - - - - -- [reverse index disabled] reverses = reverseResource - - - - - - - {- [reverse index disabled] - -------------------------------------------------------------------------------- - -- Reverse - serveReverse :: Bool -> DynamicPath -> ServerPart Response - serveReverse isRecent dpath = - htmlResponse $ - withPackageId dpath $ \pkgid -> do - let pkgname = packageName pkgid - rdisp <- case packageVersion pkgid of - Version [] [] -> withPackageAll pkgname $ \_ -> revPackageName pkgname - _ -> withPackageVersion pkgid $ \_ -> revPackageId pkgid - render <- (if isRecent then renderReverseRecent else renderReverseOld) pkgname rdisp - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ - Pages.reversePackageRender pkgid (corePackageIdUri "") revr isRecent render - - serveReverseFlat :: DynamicPath -> ServerPart Response - serveReverseFlat dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname _ -> do - revCount <- query $ GetReverseCount pkgname - pairs <- revPackageFlat pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Flattened reverse dependencies") $ - Pages.reverseFlatRender pkgname (corePackageNameUri "") revr revCount pairs - - serveReverseStats :: DynamicPath -> ServerPart Response - serveReverseStats dpath = htmlResponse $ - withPackageAllPath dpath $ \pkgname pkgs -> do - revCount <- query $ GetReverseCount pkgname - return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ "Reverse dependency statistics") $ - Pages.reverseStatsRender pkgname (map packageVersion pkgs) (corePackageIdUri "") revr revCount - - serveReverseList :: DynamicPath -> ServerPart Response - serveReverseList _ = do - let revr = reverseResource revs - triple <- sortedRevSummary revs - hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex - return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ - Pages.reversePackagesRender (corePackageNameUri "") revr hackCount triple - -} - -------------------------------------------------------------------------------- -- Additional package indices @@ -468,6 +410,8 @@ mkHtmlCore :: ServerEnv -> DistroFeature -> PackageContentsFeature -> HtmlTags + -> HtmlReverse + -> ReverseFeature -> HtmlPreferred -> AsyncCache Response -> AsyncCache Response @@ -495,6 +439,8 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} DistroFeature{queryPackageStatus} PackageContentsFeature{packageRender} HtmlTags{..} + HtmlReverse{..} + ReverseFeature{queryReverseDeps, revJSON} HtmlPreferred{..} cachePackagesPage cacheNamesPage @@ -536,6 +482,16 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , (extendResource searchPackagesResource) { resourceGet = [("html", serveBrowsePage)] } + , (resourceAt "/packages/graph.json" ) { + resourceDesc = [(GET, "Show JSON of package dependency information")] + , resourceGet = [("json", + serveGraphJSON)] + } + , (resourceAt "/packages/graph" ) { + resourceDesc = [(GET, "Show graph of package dependency information")] + , resourceGet = [("html", + serveGraph)] + } , (extendResource $ corePackagesPage cores) { resourceDesc = [(GET, "Show package index")] , resourceGet = [("html", const $ readAsyncCache cachePackagesPage)] @@ -574,6 +530,19 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} <> noscriptFormRendered ] + serveGraphJSON :: DynamicPath -> ServerPartE Response + serveGraphJSON _ = do + graph <- revJSON + --TODO: use proper type for graph with ETag + cacheControl [Public, maxAgeMinutes 30] (etagFromHash graph) + ok . toResponse $ graph + + serveGraph :: DynamicPath -> ServerPartE Response + serveGraph _ = do + cacheControlWithoutETag [Public, maxAgeDays 1] -- essentially static + template <- getTemplate templates "graph.html" + return $ toResponse $ template [] + -- Currently the main package page is thrown together by querying a bunch -- of features about their attributes for the given package. It'll need -- reorganizing to look aesthetic, as opposed to the sleek and simple current @@ -601,6 +570,7 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} userRating <- case auth of Just (uid,_) -> pkgUserVote pkgname uid; _ -> return Nothing mdoctarblob <- queryDocumentation realpkg tags <- queryTagsForPackage pkgname + rdeps <- queryReverseDeps pkgname deprs <- queryGetDeprecatedFor pkgname mreadme <- makeReadme render hasDocs <- queryHasDocumentation documentationFeature realpkg @@ -652,6 +622,9 @@ mkHtmlCore ServerEnv{serverBaseURI, serverBlobStore} , "hasExecOnly" $= (not . hasLibs) pkgdesc && (not . null) execs , "userRating" $= userRating , "score" $= pkgScore + , "hasrdeps" $= not (rdeps == ([],[])) + , "rdeps" $= renderPkgPageDeps rdeps + , "rdepsummary" $= renderDeps pkgname rdeps , "buildStatus" $= buildStatus , "hasDocs" $= hasDocs , "install" $= install @@ -1998,3 +1971,97 @@ htmlGroupResource UserFeature{..} r@(GroupResource groupR userR getGroup) = groupDeleteUser group dpath goToList dpath goToList dpath = seeOther (renderResource' (groupResource r) dpath) (toResponse ()) + +{------------------------------------------------------------------------------- + Reverse +-------------------------------------------------------------------------------} +data HtmlReverse = HtmlReverse { + htmlReverseResource :: [Resource] + } + +mkHtmlReverse :: HtmlUtilities + -> CoreFeature + -> VersionsFeature + -> ListFeature + -> ReverseFeature + -> ReverseHtmlUtil + -> HtmlReverse +mkHtmlReverse HtmlUtilities{..} + CoreFeature{ coreResource = CoreResource{ + packageInPath + , lookupPackageName + , corePackageIdUri + , corePackageNameUri + }, + queryGetPackageIndex + } + VersionsFeature{withPackageVersion} + ListFeature{} + ReverseFeature{..} + ReverseHtmlUtil{..} + = HtmlReverse{..} + where + htmlReverseResource = [ + (extendResource $ reversePackage reverseResource) { + resourceGet = [("html", serveReverse OnlyLatest)] + } + , (extendResource $ reversePackageOld reverseResource) { + resourceGet = [("html", serveReverse OnlyOlder)] + } + ,(extendResource $ reversePackageFlat reverseResource) { + resourceGet = [("html", serveReverseFlat)] + } + , (extendResource $ reversePackageVerbose reverseResource) { + resourceGet = [("html", serveReverseVerbose)] + } + , (extendResource $ reversePackages reverseResource) { + resourceGet = [("html", serveReverseList)] + } + ] + + + serveReverse :: LatestOrOld -> DynamicPath -> ServerPartE Response + serveReverse isRecent dpath = do + pkgid <- packageInPath dpath + let pkgname = pkgName pkgid + rdisp <- if nullVersion == packageVersion pkgid + then lookupPackageName pkgname *> revPackageName pkgname + else withPackageVersion pkgid $ \_ -> revPackageId pkgid + render <- (if isRecent == OnlyLatest then renderReverseRecent else renderReverseOld) pkgname rdisp + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependencies ") $ + reversePackageRender pkgid (corePackageIdUri "") isRecent render + + redirectIfVersion uriGen pkgid = + if packageVersion pkgid /= nullVersion + then do + let newUri = uriGen reverseResource "" (packageName pkgid) + seeOther newUri () + else pure () + + serveReverseFlat :: DynamicPath -> ServerPartE Response + serveReverseFlat dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseFlatUri pkg + let pkgname = pkgName pkg + revCount <- revPackageStats pkgname + pairs <- revPackageFlat pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Flattened reverse dependencies") $ + reverseFlatRender pkgname (corePackageNameUri "") revCount pairs + + serveReverseVerbose :: DynamicPath -> ServerPartE Response + serveReverseVerbose dpath = do + pkg <- packageInPath dpath + redirectIfVersion reverseVerboseUri pkg + let pkgname = pkgName pkg + pkgids <- lookupPackageName pkgname + revCount <- revPackageStats pkgname + versions <- revForEachVersion pkgname + return $ toResponse $ Resource.XHtml $ hackagePage (display pkgname ++ " - Reverse dependency statistics") $ + reverseVerboseRender pkgname (map packageVersion pkgids) (corePackageIdUri "") revCount versions + + serveReverseList :: DynamicPath -> ServerPartE Response + serveReverseList _ = do + namesWithCounts <- revCountForAllPackages + hackCount <- PackageIndex.indexSize <$> queryGetPackageIndex + return $ toResponse $ Resource.XHtml $ hackagePage "Reverse dependencies" $ + reversePackagesRender (corePackageNameUri "") hackCount namesWithCounts diff --git a/src/Distribution/Server/Features/Html/HtmlUtilities.hs b/src/Distribution/Server/Features/Html/HtmlUtilities.hs index 3c1d616da..c03e1e5ea 100644 --- a/src/Distribution/Server/Features/Html/HtmlUtilities.hs +++ b/src/Distribution/Server/Features/Html/HtmlUtilities.hs @@ -26,6 +26,8 @@ data HtmlUtilities = HtmlUtilities { , makeRow :: PackageItem -> Html , renderTags :: Set Tag -> [Html] , renderReviewTags :: Set Tag -> (Set Tag, Set Tag) -> PackageName -> [Html] + , renderDeps :: PackageName -> ([PackageName], [PackageName]) -> Html + , renderPkgPageDeps :: ([PackageName], [PackageName]) -> Html } htmlUtilities :: CoreFeature -> PackageCandidatesFeature -> TagsFeature -> UserFeature -> HtmlUtilities @@ -46,6 +48,7 @@ htmlUtilities CoreFeature{coreResource} makeRow item = tr << [ td $ itemNameHtml , td $ toHtml $ show $ itemDownloads item , td $ toHtml $ show $ itemVotes item + , td $ toHtml $ show $ itemRevDepsCount item , td $ toHtml $ itemDesc item , td $ " (" +++ renderTags (itemTags item) +++ ")" , td $ toHtml $ formatTime defaultTimeLocale "%F" (itemLastUpload item) @@ -97,3 +100,22 @@ htmlUtilities CoreFeature{coreResource} cores = coreResource + + renderPkgPageDeps :: ([PackageName], [PackageName])-> Html + renderPkgPageDeps (direct, indirect) = + map toHtml [show (length direct), " direct", ", ", show (length indirect), " indirect "] +++ + thespan ! [thestyle "font-size: small", theclass "revdepdetails"] + << (" [" +++ anchor ! [href ""] << "details" +++ "]") + + renderDeps :: PackageName -> ([PackageName], [PackageName])-> Html + renderDeps pkg (direct, indirect) = + (if null direct then (toHtml "") else summary "Direct" direct) +++ + (if null indirect then (toHtml "") else summary "Indirect" indirect) +++ + detailsLink + where + summary title_ dep = thediv << [ bold (toHtml title_), br + , p << intersperse (toHtml ", ") (map packageNameLink dep) + ] + detailsLink = thespan ! [thestyle "font-size: small"] + << (" [" +++ anchor ! [href detailURL] << "details" +++ "]") + detailURL = "/package/" ++ unPackageName pkg ++ "/reverse" diff --git a/src/Distribution/Server/Features/PackageList.hs b/src/Distribution/Server/Features/PackageList.hs index 1a719fc22..ffea264e6 100644 --- a/src/Distribution/Server/Features/PackageList.hs +++ b/src/Distribution/Server/Features/PackageList.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE RankNTypes, RecordWildCards, NamedFieldPuns #-} module Distribution.Server.Features.PackageList ( ListFeature(..), initListFeature, @@ -9,7 +9,7 @@ module Distribution.Server.Features.PackageList ( import Distribution.Server.Framework import Distribution.Server.Features.Core --- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies +import Distribution.Server.Features.ReverseDependencies import Distribution.Server.Features.Votes import Distribution.Server.Features.DownloadCount import Distribution.Server.Features.Tags @@ -23,7 +23,6 @@ import qualified Distribution.Server.Packages.PackageIndex as PackageIndex import Distribution.Server.Util.CountingMap (cmFind) import Distribution.Server.Packages.Types --- [reverse index disabled] import Distribution.Server.Packages.Reverse import Distribution.Server.Users.Types import Distribution.Package @@ -75,7 +74,7 @@ data PackageItem = PackageItem { itemDownloads :: !Int, -- The number of direct revdeps. (Likewise.) -- also: distinguish direct/flat? - -- [reverse index disabled] itemRevDepsCount :: !Int, + itemRevDepsCount :: !Int, -- Whether there's a library here. itemHasLibrary :: !Bool, -- How many executables (>=0) this package has. @@ -85,23 +84,23 @@ data PackageItem = PackageItem { -- How many benchmarks (>=0) this package has. itemNumBenchmarks :: !Int, -- Last upload date - itemLastUpload :: !UTCTime + itemLastUpload :: !UTCTime, -- Hotness: a more heuristic way to sort packages. presently non-existent. - --itemHotness :: Int + itemHotness :: !Float } instance MemSize PackageItem where - memSize (PackageItem a b c d e f g h i j k l) = memSize12 a b c d e f g h i j k l + memSize (PackageItem a b c d e f g h i j k l m n) = memSize11 a b c d e f g h i j (k, l, m, n) emptyPackageItem :: PackageName -> PackageItem emptyPackageItem pkg = PackageItem pkg Set.empty Nothing "" [] - 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) + 0 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) 0 initListFeature :: ServerEnv -> IO (CoreFeature - -- [reverse index disabled] -> ReverseFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -114,7 +113,7 @@ initListFeature _env = do itemUpdate <- newHook return $ \core@CoreFeature{..} - -- [reverse index disabled] revs + revs@ReverseFeature{revPackageStats, reverseHook} download votesf@VotesFeature{..} tagsf@TagsFeature{..} @@ -123,7 +122,7 @@ initListFeature _env = do uploads@UploadFeature{..} -> do let (feature, modifyItem, updateDesc) = - listFeature core download votesf tagsf versions users uploads + listFeature core revs download votesf tagsf versions users uploads itemCache itemUpdate registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, _) -> @@ -143,15 +142,12 @@ initListFeature _env = do runHook_ itemUpdate (Set.singleton pkgname) Nothing -> return () - {- [reverse index disabled] - votesf@VotesFeature{..} - registerHook (reverseUpdateHook revs) $ \mrev -> do - let pkgs = Map.keys mrev + registerHook reverseHook $ \pkgids -> do + let pkgs = map pkgName pkgids forM_ pkgs $ \pkgname -> do - revCount <- query . GetReverseCount $ pkgname + revCount <- revPackageStats pkgname modifyItem pkgname (updateReverseItem revCount) - runHook' itemUpdate $ Set.fromDistinctAscList pkgs - -} + runHook_ itemUpdate $ Set.fromDistinctAscList pkgs registerHook votesUpdated $ \(pkgname, _) -> do votes <- pkgNumScore pkgname @@ -172,6 +168,7 @@ initListFeature _env = do listFeature :: CoreFeature + -> ReverseFeature -> DownloadFeature -> VotesFeature -> TagsFeature @@ -185,6 +182,7 @@ listFeature :: CoreFeature PackageName -> IO ()) listFeature CoreFeature{..} + ReverseFeature{revPackageStats} DownloadFeature{..} VotesFeature{..} TagsFeature{..} @@ -249,7 +247,7 @@ listFeature CoreFeature{..} constructItem :: PkgInfo -> IO (PackageName, PackageItem) constructItem pkg = do let pkgname = packageName pkg - -- [reverse index disabled] revCount <- query . GetReverseCount $ pkgname + revCount <- revPackageStats pkgname users <- queryGetUserDb tags <- queryTagsForPackage pkgname downs <- recentPackageDownloads @@ -262,9 +260,10 @@ listFeature CoreFeature{..} , itemMaintainer = map (userIdToName users) (UserIdSet.toList maintainers) , itemDeprecated = deprs , itemDownloads = cmFind pkgname downs - -- [reverse index disabled] , itemRevDepsCount = directReverseCount revCount , itemVotes = votes , itemLastUpload = fst (pkgOriginalUploadInfo pkg) + , itemRevDepsCount = directCount revCount + , itemHotness = votes + fromIntegral (cmFind pkgname downs) + fromIntegral (directCount revCount)*2 } ------------------------------ @@ -308,7 +307,8 @@ updateTagItem tags item = updateVoteItem :: Float -> PackageItem -> PackageItem updateVoteItem score item = item { - itemVotes = score + itemVotes = score, + itemHotness = fromIntegral (itemRevDepsCount item)*2 + score + fromIntegral (itemDownloads item) } updateDeprecation :: Maybe [PackageName] -> PackageItem -> PackageItem @@ -317,13 +317,13 @@ updateDeprecation pkgs item = itemDeprecated = pkgs } -{- [reverse index disabled] updateReverseItem :: ReverseCount -> PackageItem -> PackageItem updateReverseItem revCount item = item { - itemRevDepsCount = directReverseCount revCount + itemRevDepsCount = directCount revCount, + itemDownloads = directCount revCount, + itemHotness = fromIntegral (itemRevDepsCount item)*2 + itemVotes item + fromIntegral (itemDownloads item) } --} updateDownload :: Int -> PackageItem -> PackageItem updateDownload count item = diff --git a/src/Distribution/Server/Features/PreferredVersions.hs b/src/Distribution/Server/Features/PreferredVersions.hs index 860c9b7c7..55b651acf 100644 --- a/src/Distribution/Server/Features/PreferredVersions.hs +++ b/src/Distribution/Server/Features/PreferredVersions.hs @@ -10,6 +10,7 @@ module Distribution.Server.Features.PreferredVersions ( classifyVersions, PreferredRender(..), + preferredStateComponent, ) where import Distribution.Server.Framework @@ -48,6 +49,7 @@ data VersionsFeature = VersionsFeature { queryGetPreferredInfo :: forall m. MonadIO m => PackageName -> m PreferredInfo, queryGetDeprecatedFor :: forall m. MonadIO m => PackageName -> m (Maybe [PackageName]), + queryGetPreferredVersions :: forall m. MonadIO m => m PreferredVersions, versionsResource :: VersionsResource, deprecatedHook :: Hook (PackageName, Maybe [PackageName]) (), @@ -60,6 +62,7 @@ data VersionsFeature = VersionsFeature { doPreferredsRender :: forall m. MonadIO m => m [(PackageName, PreferredRender)], doDeprecatedsRender :: forall m. MonadIO m => m [(PackageName, [PackageName])], + withPackageVersion :: forall a. PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a, withPackagePreferred :: forall a. PackageId -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a, withPackagePreferredPath :: forall a. DynamicPath -> (PkgInfo -> [PkgInfo] -> ServerPartE a) -> ServerPartE a } @@ -156,6 +159,9 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } queryGetDeprecatedFor :: MonadIO m => PackageName -> m (Maybe [PackageName]) queryGetDeprecatedFor name = queryState preferredState (GetDeprecatedFor name) + queryGetPreferredVersions :: MonadIO m => m PreferredVersions + queryGetPreferredVersions = queryState preferredState GetPreferredVersions + updateDeprecatedTags = do pkgs <- deprecatedMap <$> queryState preferredState GetPreferredVersions setCalculatedTag (Tag "deprecated") (Map.keysSet pkgs) @@ -264,6 +270,17 @@ versionsFeature ServerEnv{ serverVerbosity = verbosity } runHook_ deprecatedHook (pkgname, deprs) updateDeprecatedTags + withPackageVersion :: PackageId -> (PkgInfo -> ServerPartE a) -> ServerPartE a + withPackageVersion pkgid func = do + pkgIndex <- queryGetPackageIndex + guard (packageVersion pkgid /= nullVersion) + case PackageIndex.lookupPackageName pkgIndex (packageName pkgid) of + [] -> packageError [MText $ "No such package in package index. ", MLink "Search for related terms instead?"$ "/packages/search?terms=" ++ (display $ pkgName pkgid)] + pkg -> case find ((== packageVersion pkgid) . packageVersion) pkg of + Nothing -> packageError [MText $ "No such package version for " ++ display (packageName pkgid)] + Just pkg' -> func pkg' + where packageError = errNotFound "Package not found" + --------------------------- -- This is a function used by the HTML feature to select the version to display. -- It could be enhanced by displaying a search page in the case of failure, diff --git a/src/Distribution/Server/Features/ReverseDependencies.hs b/src/Distribution/Server/Features/ReverseDependencies.hs index e25eb5c77..d2ebadb72 100644 --- a/src/Distribution/Server/Features/ReverseDependencies.hs +++ b/src/Distribution/Server/Features/ReverseDependencies.hs @@ -1,247 +1,293 @@ -{-# LANGUAGE RankNTypes, RecordWildCards #-} +{-# LANGUAGE DeriveGeneric, RankNTypes, NamedFieldPuns, RecordWildCards #-} module Distribution.Server.Features.ReverseDependencies ( - ReverseFeature, - reverseResource, + ReverseCount(..), + ReverseFeature(..), + ReversePageRender(..), + ReverseRender(..), ReverseResource(..), - reverseUpdateHook, initReverseFeature, - ReverseRender(..), - ReversePageRender(..), - revPackageId, - revPackageName, - renderReverseRecent, - renderReverseOld, - revPackageFlat, - revPackageStats, - revPackageSummary, - revSummary, - sortedRevSummary + reverseFeature ) where -import Distribution.Server.Acid (query, update) import Distribution.Server.Framework -import Distribution.Server.Framework.BackupRestore -import Distribution.Server.Framework.BackupDump (testRoundtripByQuery) import Distribution.Server.Features.Core import Distribution.Server.Features.PreferredVersions - -import Distribution.Server.Packages.State -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred -import qualified Distribution.Server.Framework.Cache as Cache - +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.PackageIndex (PackageIndex, packageNames, allPackagesByName) +import Distribution.Server.Packages.Types (PkgInfo, pkgInfoId) +import Distribution.Server.Features.ReverseDependencies.State import Distribution.Package import Distribution.Text (display) -import Distribution.Version - -import Data.List (mapAccumL, sortBy) -import Data.Maybe (catMaybes) -import Data.Function (fix, on) -import Data.Map (Map) +import Distribution.Version (Version) + +import Control.Monad.Catch (MonadThrow, MonadCatch) +import Data.Aeson +import Data.ByteString.Lazy (ByteString) +import Data.Containers.ListUtils (nubOrd) +import Data.List (mapAccumL, sortOn) +import Data.Maybe (catMaybes, fromJust) +import Data.Function (fix) +import qualified Data.Bimap as Bimap +import qualified Data.Array as Arr +import qualified Data.Graph as Gr import qualified Data.Map as Map +import Data.Set (Set) import qualified Data.Set as Set -import Control.Monad (liftM, forever) -import Control.Monad.Trans (MonadIO) -import Control.Concurrent (forkIO) -import Control.Concurrent.Chan +import GHC.Generics hiding (packageName) +import GHC.Stack data ReverseFeature = ReverseFeature { reverseFeatureInterface :: HackageFeature, reverseResource :: ReverseResource, - reverseUpdateHook :: Hook (Map PackageName [Version] -> IO ()) + + reverseHook :: Hook [PackageId] (), + + queryReverseDeps :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]), + revPackageId :: forall m. (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay, + revPackageName :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay, + renderReverseRecent :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, + renderReverseOld :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, + revPackageFlat :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)], + revPackageStats :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount, + revCountForAllPackages :: forall m. (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)], + revJSON :: forall m. (MonadIO m, MonadThrow m) => m ByteString, + revDisplayInfo :: forall m. MonadIO m => m VersionIndex, + revForEachVersion :: forall m. (MonadIO m, MonadThrow m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) } instance IsHackageFeature ReverseFeature where getFeatureInterface = reverseFeatureInterface - data ReverseResource = ReverseResource { reversePackage :: Resource, reversePackageOld :: Resource, - reversePackageAll :: Resource, - reversePackageStats :: Resource, + reversePackageFlat :: Resource, + reversePackageVerbose :: Resource, reversePackages :: Resource, - reversePackagesAll :: Resource, reverseUri :: String -> PackageId -> String, reverseNameUri :: String -> PackageName -> String, reverseOldUri :: String -> PackageId -> String, - reverseOldNameUri :: String -> PackageName -> String, - reverseAllUri :: String -> PackageName -> String, - reverseStatsUri :: String -> PackageName -> String, - reversesUri :: String -> String, - reversesAllUri :: String -> String + reverseFlatUri :: String -> PackageName -> String, + reverseVerboseUri :: String -> PackageName -> String } initReverseFeature :: ServerEnv -> IO (CoreFeature + -> VersionsFeature -> IO ReverseFeature) -initReverseFeature ServerEnv{serverVerbosity = verbosity} = do - revChan <- newChan - registerHook (packageAddHook core) $ \pkg -> writeChan revChan $ - update $ AddReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageRemoveHook core) $ \pkg -> writeChan revChan $ - update $ RemoveReversePackage (packageId pkg) (getAllDependencies pkg) - registerHook (packageChangeHook core) $ \pkg pkg' -> writeChan revChan $ - update $ ChangeReversePackage (packageId pkg) - (getAllDependencies pkg) (getAllDependencies pkg') - - revHook <- newHook - let select (_, b, _) = b - sortedRevs = fmap (sortBy $ on (flip compare) select) revSummary - revTopCache <- Cache.newCacheable =<< sortedRevs - registerHook revHook $ \_ -> Cache.putCache revTopCache =<< sortedRevs - - return $ \core -> do - let feature = reverseFeature core - revChan revHook revTopCache +initReverseFeature _ = do + updateReverse <- newHook + + return $ \CoreFeature{queryGetPackageIndex,packageChangeHook} + VersionsFeature{queryGetPreferredVersions} -> do + idx <- queryGetPackageIndex + memState <- newMemStateWHNF =<< constructReverseIndex idx + + let feature = reverseFeature queryGetPackageIndex queryGetPreferredVersions memState updateReverse + + registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, mpkginfo) -> + case mpkginfo of + Nothing -> return () --PackageRemoveHook + Just pkginfo -> do + index <- queryGetPackageIndex + r <- readMemState memState + added <- addPackage (packageName pkgid) (getAllDependencies pkginfo index) r + writeMemState memState added + runHook_ updateReverse [pkgid] return feature -reverseFeature :: CoreFeature - -> Chan (IO (Map PackageName [Version])) - -> Hook (Map PackageName [Version] -> IO ()) - -> Cache.Cache [(PackageName, Int, Int)] +data ReverseRender = ReverseRender { + rendRevPkg :: PackageId, + rendRevStatus :: Maybe VersionStatus, + rendRevCount :: Int +} deriving (Show, Eq, Ord) + +data ReversePageRender = ReversePageRender { + rendRevList :: [ReverseRender], + rendFilterCount :: (Int, Int), + rendPageTotal :: Int +} + +-- data Node = Node {id::Int, label::String} deriving Generic +data Edge = Edge { + id::Int, + name::String, + deps::[String] + } deriving Generic +-- data JGraph = JGraph { nodes::[Node], edges::[Edge]} deriving Generic +-- instance ToJSON Node +instance ToJSON Edge +-- instance ToJSON JGraph +-- instance ToJSON PackageName + +reverseFeature :: IO (PackageIndex PkgInfo) + -> IO PreferredVersions + -> MemState ReverseIndex + -> Hook [PackageId] () -> ReverseFeature -reverseFeature CoreFeature{..} - reverseStream reverseUpdateHook reverseTopCache +reverseFeature queryGetPackageIndex + queryGetPreferredVersions + reverseMemState + reverseHook = ReverseFeature{..} where reverseFeatureInterface = (emptyHackageFeature "reverse") { - featureResources = map ($reverseResource) [] - , featurePostInit = forkIO transferReverse >> return () - , featureDumpRestore = Just (return [], restoreBackup, testRoundtripByQuery (query GetReverseIndex)) + featureResources = map ($ reverseResource) [] + , featurePostInit = initReverseIndex + , featureState = [] + , featureCaches = [ + CacheComponent { + cacheDesc = "reverse index", + getCacheMemSize = memSize <$> readMemState reverseMemState + } + ] } - transferReverse = forever $ do - revFunc <- readChan reverseStream - modded <- revFunc - runHook' reverseUpdateHook modded - - --TODO: this isn't a restore! - -- do we need a post init/restore hook for initialising caches? - restoreBackup = RestoreBackup - { restoreEntry = \_ -> return $ Right restoreBackup - , restoreFinalize = return $ Right restoreBackup - , restoreComplete = do - putStrLn "Calculating reverse dependencies" - index <- fmap packageList $ query GetPackagesState - let revs = constructReverseIndex index - update $ ReplaceReverseIndex revs - } + initReverseIndex :: IO () + initReverseIndex = do + index <- liftIO queryGetPackageIndex + -- We build the proper index earlier, this just fires the reverse hooks + let allPackages = map pkgInfoId $ concat $ allPackagesByName index + runHook_ reverseHook allPackages + reverseResource = fix $ \r -> ReverseResource { reversePackage = resourceAt "/package/:package/reverse.:format" , reversePackageOld = resourceAt "/package/:package/reverse/old.:format" - , reversePackageAll = resourceAt "/package/:package/reverse/all.:format" - , reversePackageStats = resourceAt "/package/:package/reverse/summary.:format" + , reversePackageFlat = resourceAt "/package/:package/reverse/flat.:format" + , reversePackageVerbose = resourceAt "/package/:package/reverse/verbose.:format" , reversePackages = resourceAt "/packages/reverse.:format" - , reversePackagesAll = resourceAt "/packages/reverse/all.:format" , reverseUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseNameUri = \format pkg -> renderResource (reversePackage r) [display pkg, format] , reverseOldUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseOldNameUri = \format pkg -> renderResource (reversePackageOld r) [display pkg, format] - , reverseAllUri = \format pkg -> renderResource (reversePackageAll r) [display pkg, format] - , reverseStatsUri = \format pkg -> renderResource (reversePackageStats r) [display pkg, format] - , reversesUri = \format -> renderResource (reversePackages r) [format] - , reversesAllUri = \format -> renderResource (reversePackagesAll r) [format] + , reverseFlatUri = \format pkg -> renderResource (reversePackageFlat r) [display pkg, format] + , reverseVerboseUri = \format pkg -> renderResource (reversePackageVerbose r) [display pkg, format] } --textRevDisplay :: ReverseDisplay -> String --textRevDisplay m = unlines . map (\(n, (v, m)) -> display n ++ "-" ++ display v ++ ": " ++ show m) . Map.toList $ m --- If VersionStatus caching is used, revPackageId and revPackageName could be --- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). -revPackageId :: MonadIO m => PackageId -> m ReverseDisplay -revPackageId pkgid = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perVersionReverse dispInfo revs pkgid - -revPackageName :: MonadIO m => PackageName -> m ReverseDisplay -revPackageName pkgname = do - dispInfo <- revDisplayInfo - revs <- liftM reverseDependencies $ query GetReverseIndex - return $ perPackageReverse dispInfo revs pkgname - -revDisplayInfo :: MonadIO m => m VersionIndex -revDisplayInfo = do - pkgIndex <- liftM packageList $ query GetPackagesState - prefs <- query GetPreferredVersions - return $ getDisplayInfo prefs pkgIndex - -data ReverseRender = ReverseRender { - rendRevPkg :: PackageId, - rendRevStatus :: Maybe VersionStatus, - rendRevCount :: Int -} deriving (Show, Eq, Ord) - -data ReversePageRender = ReversePageRender { - rendRevList :: [ReverseRender], - rendFilterCount :: (Int, Int), - rendPageTotal :: Int -} - -renderReverseWith :: MonadIO m => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender -renderReverseWith pkg rev filterFunc = do - counts <- liftM reverseCount $ query GetReverseIndex - let toRender (i, i') (pkgname, (version, status)) = case filterFunc status of - False -> (,) (i, i'+1) Nothing - True -> (,) (i+1, i') $ Just $ ReverseRender { - rendRevPkg = PackageIdentifier pkgname version, - rendRevStatus = status, - rendRevCount = maybe 0 directReverseCount $ Map.lookup pkgname counts - } - (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) - pkgCount = maybe 0 directReverseCount $ Map.lookup pkg counts - return $ ReversePageRender (catMaybes rlist) res pkgCount - -renderReverseRecent :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> False - Nothing -> False - _ -> True - -renderReverseOld :: MonadIO m => PackageName -> ReverseDisplay -> m ReversePageRender -renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of - Just DeprecatedVersion -> True - Nothing -> True - _ -> False - --- This could also differentiate between direct and indirect dependencies --- with a bit more calculation. -revPackageFlat :: MonadIO m => PackageName -> m [(PackageName, Int)] -revPackageFlat pkgname = do - index <- query GetReverseIndex - let counts = reverseCount index - count pkg = maybe 0 flattenedReverseCount $ Map.lookup pkg counts - pkgs = maybe [] Set.toList $ Map.lookup pkgname $ flattenedReverse index - return $ map (\pkg -> (pkg, count pkg)) pkgs - -revPackageStats :: MonadIO m => PackageName -> m ReverseCount -revPackageStats = query . GetReverseCount - -revPackageSummary :: MonadIO m => PackageId -> m (Int, Int) -revPackageSummary (PackageIdentifier pkgname version) = do - ReverseCount direct _ versions <- revPackageStats pkgname - return (direct, Map.findWithDefault 0 version versions) - --- This returns a list of (package name, direct dependencies, flat dependencies) --- for all packages. An interesting fact: it even does so for packages which --- don't exist in the index, except the latter two fields are always zero. This is --- because no versions of these packages exist, so the union of no versions is --- still no versions. TODO: use this fact to make an index of dependencies which --- are not in Hackage at all, which might be useful for fixing accidentally --- broken packages. -revSummary :: MonadIO m => m [(PackageName, Int, Int)] -revSummary = do - counts <- liftM reverseCount $ query GetReverseIndex - return $ map (\(pkg, ReverseCount direct flat _) -> (pkg, direct, flat)) $ Map.toList counts - -sortedRevSummary :: MonadIO m => ReverseFeature -> m [(PackageName, Int, Int)] -sortedRevSummary revs = Cache.getCache $ reverseTopCache revs - + -- If VersionStatus caching is used, revPackageId and revPackageName could be + -- reduced to a single map lookup (see Distribution.Server.Packages.Reverse). + queryReverseIndex :: MonadIO m => m ReverseIndex + queryReverseIndex = readMemState reverseMemState + + queryReverseDeps :: (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]) + queryReverseDeps pkgname = do + ms <- readMemState reverseMemState + rdeps <- getDependencies pkgname ms + rdepsall <- getDependenciesFlat pkgname ms + let indirect = Set.difference rdepsall rdeps + return (Set.toList rdeps, Set.toList indirect) + + revPackageId :: (HasCallStack, MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay + revPackageId pkgid = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + perVersionReverse dispInfo pkgIndex revs pkgid + + revPackageName :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay + revPackageName pkgname = do + dispInfo <- revDisplayInfo + pkgIndex <- liftIO queryGetPackageIndex + revs <- queryReverseIndex + perPackageReverse dispInfo pkgIndex revs pkgname + + revJSON :: (MonadIO m, MonadThrow m) => m ByteString + revJSON = do + ReverseIndex revdeps nodemap <- queryReverseIndex + let assoc = takeWhile (\(a,_) -> a < Bimap.size nodemap) $ Arr.assocs . Gr.transposeG $ revdeps + nodeToString node = unPackageName (nodemap Bimap.!> node) + -- nodes = map (uncurry Node) $ map (\n -> (fst n, nodeToString (fst n))) assoc + edges = map (\(a,b) -> Edge a (nodeToString a) (map (\x-> nodeToString x) b)) assoc + return $ encode edges + + revDisplayInfo :: MonadIO m => m VersionIndex + revDisplayInfo = do + pkgIndex <- liftIO queryGetPackageIndex + prefs <- liftIO queryGetPreferredVersions + return $ getDisplayInfo prefs pkgIndex + + renderReverseWith :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender + renderReverseWith pkg rev filterFunc = do + let rev' = map fst $ Map.toList rev + directCounts <- mapM revDirectCount (pkg:rev') + let counts = zip (pkg:rev') directCounts + toRender (i, i') (pkgname, (version, status)) = if filterFunc status then (,) (i+1, i') $ Just ReverseRender { + rendRevPkg = PackageIdentifier pkgname version, + rendRevStatus = status, + rendRevCount = fromJust $ lookup pkgname counts + } else (,) (i, i'+1) Nothing + (res, rlist) = mapAccumL toRender (0, 0) (Map.toList rev) + pkgCount = fromJust $ lookup pkg counts + return $ ReversePageRender (catMaybes rlist) res pkgCount + + renderReverseRecent :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseRecent pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> False + Nothing -> False + _ -> True + + renderReverseOld :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender + renderReverseOld pkg rev = renderReverseWith pkg rev $ \status -> case status of + Just DeprecatedVersion -> True + Nothing -> True + _ -> False + + -- -- This could also differentiate between direct and indirect dependencies + -- -- with a bit more calculation. + revPackageFlat :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)] + revPackageFlat pkgname = do + memState <- readMemState reverseMemState + deps <- getDependenciesFlat pkgname memState + let depList = Set.toList deps + counts <- mapM (`getTotalCount` memState) depList + return $ zip depList counts + + revPackageStats :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m ReverseCount + revPackageStats pkgname = do + (direct, transitive) <- getReverseCount pkgname =<< readMemState reverseMemState + return $ ReverseCount direct transitive + + revDirectCount :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m Int + revDirectCount pkgname = do + getDirectCount pkgname =<< readMemState reverseMemState + + -- This returns a list of (package name, direct dependencies, flat dependencies) + -- for all packages. An interesting fact: it even does so for packages which + -- don't exist in the index, except the latter two fields are always zero. This is + -- because no versions of these packages exist, so the union of no versions is + -- still no versions. TODO: use this fact to make an index of dependencies which + -- are not in Hackage at all, which might be useful for fixing accidentally + -- broken packages. + -- + -- The returned list is sorted ascendingly on directCount (see ReverseCount). + revCountForAllPackages :: (HasCallStack, MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)] + revCountForAllPackages = do + index <- liftIO queryGetPackageIndex + let pkgnames = packageNames index + counts <- mapM revPackageStats pkgnames + return . sortOn (directCount . snd) $ zip pkgnames counts + + revForEachVersion :: (MonadThrow m, MonadIO m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) + revForEachVersion pkg = do + ReverseIndex revs nodemap <- readMemState reverseMemState + index <- liftIO queryGetPackageIndex + nodeid <- Bimap.lookup pkg nodemap + revDepNames <- mapM (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) + let -- The key is the version of 'pkg', and the values are specific + -- package versions that accept this version of pkg specified in the key + revDepVersions :: [(Version, Set PackageIdentifier)] + revDepVersions = do + x <- nubOrd revDepNames + pkginfo <- PackageIndex.lookupPackageName index pkg + pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x) + pure $ Map.fromListWith Set.union revDepVersions diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index 4a95db924..97f423180 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -1,458 +1,262 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE RankNTypes #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} - -module Distribution.Server.Features.ReverseDependencies.State where - -import Distribution.Server.Packages.Types -import Distribution.Server.Packages.State () -import Distribution.Server.Packages.Preferred -import Distribution.Server.Packages.PackageIndex (PackageIndex) -import qualified Distribution.Server.Packages.PackageIndex as PackageIndex - -import Distribution.Package -import Distribution.PackageDescription -import Distribution.Version +{-# LANGUAGE RankNTypes, DeriveGeneric, TypeSynonymInstances, DeriveDataTypeable, GeneralizedNewtypeDeriving, TypeFamilies, TemplateHaskell, ScopedTypeVariables #-} + +module Distribution.Server.Features.ReverseDependencies.State + ( ReverseIndex(..) + , ReverseDisplay + , ReverseCount(..) + , VersionIndex + , addPackage + , constructReverseIndex + , dependsOnPkg + , emptyReverseIndex + , getAllDependencies + , getDependencies + , getDependenciesFlat + , getDependenciesFlatRaw + , getDependenciesRaw + , getDirectCount + , getDisplayInfo + , getReverseCount + , getTotalCount + , perPackageReverse + , perVersionReverse + , suc + ) + where -import Data.Acid (Query, Update, makeAcidic) -import Data.List (foldl', union) -import Data.Maybe (maybeToList, fromMaybe) -import Data.SafeCopy (base, deriveSafeCopy) -import Data.Typeable (Typeable) -import Data.Map (Map) +import Prelude hiding (lookup) + +import Control.Monad (forM) +import Control.Monad.Catch +import Control.Monad.Reader (MonadIO) +import qualified Data.Array as Arr ((!), assocs, accumArray) +import Data.Bimap (Bimap, lookup, lookupR) +import qualified Data.Bimap as Bimap +import Data.Containers.ListUtils (nubOrd) +import Data.List (union) +import Data.Map (Map) import qualified Data.Map as Map -import Data.Set (Set) +import Data.Maybe (maybeToList, mapMaybe) +import Data.SafeCopy hiding (Version) import qualified Data.Set as Set +import Data.Set (Set, fromList, toList, delete) +import Data.Typeable (Typeable) +import Data.Graph (Graph, Vertex) +import qualified Data.Graph as Gr +import GHC.Stack (HasCallStack, callStack, prettyCallStack) + +import Distribution.Package +import Distribution.PackageDescription +import Distribution.Server.Packages.Types +import Distribution.Server.Framework.MemSize +import Distribution.Server.Features.PreferredVersions.State +import Distribution.Server.Packages.PackageIndex (PackageIndex) +import qualified Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Version -import Data.STRef -import Control.Monad.ST -import Control.Monad.State (put, get) -import Control.Monad.Reader (ask, asks) - --- The main reverse dependencies map is a drawn-out Map PackageId PackageId, --- with an extra component to store ranges (all ranges that *could* be revdeps, --- even if no packages in the index currently satisfy those ranges). --- --- For selected entries of the map (foo, (2.0, (bar, [1.0]), (bar, (1.0, <3)))): --- This means that bar-1.0 depends on a version of foo <3, and foo 2.0 meets this criterion. -type RevDeps = Map PackageName (Map Version (Map PackageName (Set Version)), Map PackageName (Map Version VersionRange)) - -type CombinedDeps = Map PackageName VersionRange - --- TODO: should this be (Maybe (Version, Maybe VersionStatus))? --- it should be possible, albeit with a bit more work, to determine all revdeps --- of a package which don't have any versions presently satisfying them. --- (for an entry (a, b) of RevDeps, take (union a \ b)) -type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) - -data ReverseIndex = ReverseIndex { - -- this field is a duplication of PackageIndex, so updates don't have - -- to use much information outside of this component, resulting in huge - -- happstack-state event files - duplicatedIndex :: PackageIndex PackageId, - -- The main reverse dependencies map - reverseDependencies :: RevDeps, +emptyReverseIndex :: ReverseIndex +emptyReverseIndex = ReverseIndex (Gr.buildG (0,0) []) Bimap.empty - -- Generated from packageNameClosure. - flattenedReverse :: Map PackageName (Set PackageName), +type NodeId = Int +type RevDeps = Graph - -- Cached counts for reverse dependencies. - reverseCount :: Map PackageName ReverseCount +data ReverseIndex = ReverseIndex { + reverseDependencies :: !RevDeps, + packageNodeIdMap :: !(Bimap PackageName NodeId) } deriving (Eq, Show, Typeable) -emptyReverseIndex :: ReverseIndex -emptyReverseIndex = ReverseIndex (PackageIndex.fromList []) - Map.empty Map.empty Map.empty - -data ReverseCount = ReverseCount { - directReverseCount :: Int, - flattenedReverseCount :: Int, - versionReverseCount :: Map Version Int -} deriving (Show, Eq, Typeable, Ord) -emptyReverseCount :: ReverseCount -emptyReverseCount = ReverseCount 0 0 Map.empty - -constructReverseIndex :: PackageIndex PkgInfo -> ReverseIndex -constructReverseIndex index = - let deps = constructRevDeps index - in updateReverseCount $ emptyReverseIndex { - duplicatedIndex = constructDupIndex index, - reverseDependencies = deps, - flattenedReverse = packageNameClosure deps - } - -constructDupIndex :: PackageIndex PkgInfo -> PackageIndex PackageId -constructDupIndex = PackageIndex.fromList - . map packageId - . PackageIndex.allPackages - -updateReverseCount :: ReverseIndex -> ReverseIndex -updateReverseCount index = - let deps = reverseDependencies index - flat = flattenedReverse index - in index { - reverseCount = flip Map.mapWithKey deps $ \pkg (versions, _) -> ReverseCount { - directReverseCount = Map.size . Map.unions $ Map.elems versions, - flattenedReverseCount = maybe 0 Set.size $ Map.lookup pkg flat, - versionReverseCount = Map.map Map.size versions - } - } - -addPackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -addPackage pkgid deps revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - (rd, rec) = registerPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -removePackage :: PackageId -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -removePackage pkgid deps revs = - let index = PackageIndex.deletePackageId pkgid (duplicatedIndex revs) - (rd, rec) = unregisterPackage (getAllVersions index) pkgid deps (reverseDependencies revs) - in (updateReverseCount $ revs { - duplicatedIndex = index, +instance MemSize ReverseIndex where + memSize (ReverseIndex a b) = memSize2 a b + + +constructReverseIndex :: (MonadCatch m, HasCallStack) => PackageIndex PkgInfo -> m ReverseIndex +constructReverseIndex index = do + let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] + revs <- constructRevDeps index nodePkgMap + pure $ + ReverseIndex { + reverseDependencies = revs, + packageNodeIdMap = nodePkgMap + } + +addPackage :: (HasCallStack, MonadCatch m, MonadIO m) => HasCallStack => PackageName -> [PackageName] + -> ReverseIndex -> m ReverseIndex +addPackage pkgid deps ri@(ReverseIndex revs nodemap) = do + let + npm = Bimap.tryInsert pkgid (Bimap.size nodemap) nodemap + new :: [(Int, [Int])] <- + forM deps $ \d -> + (,) <$> looky d npm <*> fmap (:[]) (looky pkgid npm) + --pure (npm ! d, [npm ! pkgid]) + let rd = insEdges (Bimap.size npm) new revs + pure $ + ri { reverseDependencies = rd, - flattenedReverse = packageNameClosure rd - }, rec) - -changePackage :: PackageId -> CombinedDeps -> CombinedDeps - -> ReverseIndex -> (ReverseIndex, Map PackageName [Version]) -changePackage pkgid deps deps' revs = - let index = PackageIndex.insert pkgid (duplicatedIndex revs) - allVersions = getAllVersions index - (rd, rec) = unregisterPackage allVersions pkgid deps (reverseDependencies revs) - (rd', rec') = registerPackage allVersions pkgid deps' rd - in (updateReverseCount $ revs { - duplicatedIndex = index, - reverseDependencies = rd', - flattenedReverse = packageNameClosure rd' - }, Map.unionWith union rec rec') --------------------------------------------------------------------------------- --- Managing the RevDeps index. - -constructRevDeps :: PackageIndex PkgInfo -> RevDeps -constructRevDeps index = foldl' (\revs pkg -> fst $ registerPackage (getAllVersions index) (packageId pkg) (getAllDependencies pkg) revs) Map.empty $ PackageIndex.allPackages index - -getAllVersions :: Package pkg => PackageIndex pkg -> PackageName -> [Version] -getAllVersions index = map packageVersion . PackageIndex.lookupPackageName index - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to include it. -registerPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -registerPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goRegister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - -- this takes each of the registered packages dependencies and puts an entry - -- of it there. e.g. a new version of base would not have much work to do - -- here because it has no dependencies - goRegister prev (pkgname, (range, versions)) = - -- revPackage encodes the dependency (name -> pkgname) in a way that can - -- be inserted into pkgname's reverse dependency mapping - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton name (Map.singleton version range) - in Map.insertWith (\(small, small') (big, big') -> (Map.unionWith (Map.unionWith Set.union) big small, Map.unionWith (flip const) big' small')) pkgname (revPackage, revRange) prev - -- this uses the package's existing reverse dependencies, regardless of - -- version, to find reverse dependencies for this version in particular. - -- e.g. a new version of base would have to recalculate the dependencies - -- of nearly all of the packages in the index - backtrace prev = case Map.lookup name prev of - Nothing -> prev - Just (vs, rs) -> - let revVersion = Map.map (Set.fromList . map fst . filter (withinRange version . snd) . Map.toList) rs - in Map.insert name (Map.insertWith (\new old -> Map.unionWith Set.union old new) version revVersion vs, rs) prev - --- | Given a package id, modify the entries of the package's dependencies in --- the reverse dependencies mapping to exclude it. -unregisterPackage :: (PackageName -> [Version]) - -> PackageId -> CombinedDeps - -> RevDeps -> (RevDeps, Map PackageName [Version]) -unregisterPackage getVersions (PackageIdentifier name version) ranges revs = - let deps = getLinkedNodes getVersions ranges - revs' = foldl' goUnregister revs $ Map.toList $ Map.intersectionWith (,) ranges deps - revs'' = backtrace revs' - in (revs'', deps) - where - pkgMap = Map.singleton name $ Set.singleton version - goUnregister prev (pkgname, (range, versions)) = - let revPackage = Map.fromList $ map (\v -> (v, pkgMap)) versions - revRange = Map.singleton pkgname (Map.fromList $ map (\v -> (v, range)) versions) - -- there are possibly better ways to go about this - in Map.differenceWith (\(a, b) (c, d) -> keepMaps - ( Map.differenceWith (\e f -> - keepMap $ Map.differenceWith (\g h -> - keepSet $ Set.difference g h) - e f) - a c - , Map.differenceWith (\e f -> - keepMap $ Map.difference e f) - b d - )) prev (Map.singleton pkgname (revPackage, revRange)) - backtrace prev = Map.update (\(vs, rs) -> keepMaps (Map.delete version vs, rs)) name prev + packageNodeIdMap = npm + } --------------------------------------------------------------------------------- --- Calculating dependencies and selecting versions - --- | Given a package, determine the packages on which it depends. --- For all such packages, return the specific versions that satisfy the --- dependency as indicated in the cabal file. -getLinkedNodes :: (PackageName -> [Version]) - -> CombinedDeps -> Map PackageName [Version] -getLinkedNodes getVersions pkgs = Map.mapWithKey (\pkg range -> selectVersions range $ getVersions pkg) pkgs - --- | Given a dependency (a package name and a version range), find all versions --- in the current package index that satisfy it. -selectVersions :: VersionRange -> [Version] -> [Version] -selectVersions range versions= filter (flip withinRange range) versions - --- | Collect all dependencies specified in a package's cabal file, considering --- all alternatives. --- --- This unions all version ranges together from different branches, which is --- imprecise but mostly good enough (a slightly better heuristic might be --- intersecting within a block, but unioning blocks together). -getAllDependencies :: PkgInfo -> CombinedDeps -getAllDependencies pkg = - let desc = pkgDesc pkg - in Map.fromListWith unionVersionRanges $ toDepsList (maybeToList $ condLibrary desc) - ++ toDepsList (map snd $ condExecutables desc) - where toDepsList :: [CondTree v [Dependency] a] -> [(PackageName, VersionRange)] - toDepsList l = [ (p, v) | Dependency p v <- concatMap harvestDependencies l ] - --- | Collect all dependencies from all branches of a condition tree. -harvestDependencies :: CondTree v [Dependency] a -> [Dependency] -harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps - where forComponent (_, iftree, elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree +constructRevDeps :: forall m. (MonadCatch m, HasCallStack) => PackageIndex PkgInfo -> Bimap PackageName NodeId -> m RevDeps +constructRevDeps index nodemap = do + let allPackages :: [PkgInfo] + allPackages = concat {- concatMap (take 5) -} $ PackageIndex.allPackagesByName index + g :: PkgInfo -> m [(NodeId, NodeId)] + g pkg = mapM (f pkg) (getAllDependencies pkg index) + f :: PkgInfo -> PackageName -> m (NodeId, NodeId) + f pkg dep = (,) <$> (lookup dep nodemap) <*> (lookup (packageName pkg) nodemap :: m NodeId) + edges <- traverse g allPackages + + pure $ Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) + +getAllDependencies :: forall pkg. Package pkg => PkgInfo -> PackageIndex pkg -> [PackageName] +getAllDependencies pkg index = + map packageName $ toDepsList (maybeToList $ condLibrary desc) ++ toDepsList (map snd $ condExecutables desc) + where + desc = pkgDesc pkg + toDepsList :: [CondTree v [Dependency] a] -> [pkg] + toDepsList l = concatMap (PackageIndex.lookupDependency index) $ concatMap harvestDependencies l + -- | Collect all dependencies from all branches of a condition tree. + harvestDependencies :: CondTree v [Dependency] a -> [Dependency] + harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps + where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + +-- | Returns [containers 0.5.0.0 to 0.6.0.1] for needle=ghc-prim-0.6.0.0 and packageHaystack=containers +-- | because these are the versions that could accept that version of ghc-prim as a dep +-- | Note that this doesn't include executables! Only the library. +dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Set PackageIdentifier +dependsOnPkg index needle packageHaystack = + fromList $ mapMaybe descPermits (PackageIndex.lookupPackageName index packageHaystack) + where + descPermits :: PkgInfo -> Maybe PackageIdentifier + descPermits pkginfo + | any toDepsList . concatMap harvestDependencies . condLibrary $ pkgDesc pkginfo = Just (packageId pkginfo) + | otherwise = Nothing + toDepsList (Dependency name versionRange _) = + packageVersion needle `withinRange` versionRange + && packageName needle == name + -- | Collect all dependencies from all branches of a condition tree. + harvestDependencies :: CondTree v [Dependency] a -> [Dependency] + harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps + where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + +---------------------------------ReverseDisplay -------------------------------------------------------------------------------- -- Calculating ReverseDisplays -type VersionIndex = (PackageName -> (PreferredInfo, [Version])) +data ReverseCount = ReverseCount + { directCount :: Int + , totalCount :: Int + } deriving (Show, Eq, Typeable, Ord) + +instance MemSize ReverseCount where + memSize (ReverseCount a b) = memSize2 a b --- TODO: this should use the secondary PackageId -> VersionRange mapping in RevDeps, --- so it gets all possible versions, not just those currently in the index. -perPackageReverse :: VersionIndex -> RevDeps -> PackageName -> ReverseDisplay -perPackageReverse indexFunc revs pkg = case Map.lookup pkg revs of - Nothing -> Map.empty - Just (dict, _) -> constructReverseDisplay indexFunc (Map.unionsWith Set.union $ Map.elems dict) +type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) -perVersionReverse :: VersionIndex -> RevDeps -> PackageId -> ReverseDisplay -perVersionReverse indexFunc revs pkg = case Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs of - Nothing -> Map.empty - Just dict -> constructReverseDisplay indexFunc dict +type VersionIndex = (PackageName -> (PreferredInfo, [Version])) -constructReverseDisplay :: VersionIndex -> Map PackageName (Set Version) -> ReverseDisplay -constructReverseDisplay indexFunc deps = - Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) deps +-- | Look up 'key', but in case of failure, fails with a more informative error message than what Bimap provides +looky :: (HasCallStack, MonadCatch m, Ord a, Ord b, Show a) => a -> Bimap a b -> m b +looky key bimap = do + res <- try (lookup key bimap) + case res of + Left (_ :: SomeException) -> throwM $ userError $ "couldn't find key " ++ show key ++ " " ++ prettyCallStack callStack + Right a -> pure a + +perPackageReverse :: (HasCallStack, MonadCatch m) => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> m (Map PackageName (Version, Maybe VersionStatus)) +perPackageReverse indexFunc index revdeps pkg = do + let pkgids = (packageVersion. packageId) <$> PackageIndex.lookupPackageName index pkg + let best :: PackageId + best = PackageIdentifier pkg (maximum pkgids) + perVersionReverse indexFunc index revdeps best + +perVersionReverse :: (HasCallStack, MonadCatch m) => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) +perVersionReverse indexFunc index (ReverseIndex revs nodemap) pkg = do + found <- lookup (packageName pkg) nodemap + -- this will be too much, since we are throwing away the specific version + revDepNames :: Set PackageName <- fromList <$> mapM (`lookupR` nodemap) (toList $ suc revs found) + let packagemap :: Map PackageName (Set Version) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x)) (toList revDepNames) + pure $ constructReverseDisplay indexFunc packagemap + +constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) +constructReverseDisplay indexFunc = + Map.mapMaybeWithKey (uncurry maybeBestVersion . indexFunc) getDisplayInfo :: PreferredVersions -> PackageIndex PkgInfo -> VersionIndex getDisplayInfo preferred index pkgname = (,) (Map.findWithDefault emptyPreferredInfo pkgname $ preferredMap preferred) (map packageVersion . PackageIndex.lookupPackageName index $ pkgname) --------------------------------------------------------------------------------- --- Keeping a cached map of selected versions. --- --- Currently it's rather quick to calculate each, and the majority of processing --- will probably go towards rendering it in HTML/JSON/whathaveyou anyway. So this --- is not used. --- --- Still, in a future Hackage where preferred-versions and deprecated versions --- are *very* widely used, incremental updates of an display index might become --- necessary. In such a future, there would be two maps in the ReverseIndex --- structure, both to ReverseDisplay from PackageName and PackageId. They would --- need to be updated with updatePackageReverse and updateVersionReverse, --- respectively, whenever a package is added, removed, or has its preferred info --- changed. - -constructPackageReverse :: VersionIndex -> RevDeps -> Map PackageName ReverseDisplay -constructPackageReverse indexFunc revs = - Map.fromList $ do - pkg <- Map.keys revs - rev <- maybeToList . keepMap $ perPackageReverse indexFunc revs pkg - return (pkg, rev) - -constructVersionReverse :: VersionIndex -> RevDeps -> Map PackageId ReverseDisplay -constructVersionReverse indexFunc revs = - Map.fromList $ do - pkg <- getNodes =<< Map.toList revs - rev <- maybeToList . keepMap $ perVersionReverse indexFunc revs pkg - return (pkg, rev) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - --- | With a package which has just been updated, make sure the version displayed --- in its reverse display is the most recent. To do this, each of its dependencies --- needs its ReverseDisplay updated. -updateReverseDisplay :: VersionIndex -> PackageName -> Set Version -> ReverseDisplay -> ReverseDisplay -updateReverseDisplay indexFunc pkgname versions revDisplay = - let toVersions = uncurry maybeBestVersion . indexFunc - in case toVersions pkgname versions of - Nothing -> revDisplay - Just status -> Map.insert pkgname status revDisplay - --- If the RevDeps index is modified through registering/unregistering packages, --- updatePackageReverse and updateVersionReverse should be given a list of --- package names/package ids distrilled from the resultant (Map PackageName --- [Version]). The idea is to sync it with the just-updated RevDeps. --- --- If a package's PreferredVersions are modified, these functions should be --- called with the same information taken from the getLinkedNodes function. --- In this case, the RevDeps data structure hasn't changed. - -updatePackageReverse :: VersionIndex -> PackageName -> [PackageName] -> RevDeps -> Map PackageName ReverseDisplay -> Map PackageName ReverseDisplay -updatePackageReverse indexFunc updated deps revs nameMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) nameMap deps - where - lookupVersions :: PackageName -> Set Version - lookupVersions pkgname = maybe Set.empty (Set.unions . map (Map.findWithDefault Set.empty updated) . Map.elems . fst) $ Map.lookup pkgname revs - alterRevDisplay :: PackageName -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgname rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgname) rev - -updateVersionReverse :: VersionIndex -> PackageName -> [PackageId] -> RevDeps -> Map PackageId ReverseDisplay -> Map PackageId ReverseDisplay -updateVersionReverse indexFunc updated deps revs pkgMap = - foldl' (\revd pkg -> Map.alter (alterRevDisplay pkg . fromMaybe Map.empty) pkg revd) pkgMap deps - where - lookupVersions :: PackageId -> Set Version - lookupVersions pkgid = maybe Set.empty (Map.findWithDefault Set.empty updated) $ Map.lookup (packageVersion pkgid) . fst =<< Map.lookup (packageName pkgid) revs - alterRevDisplay :: PackageId -> ReverseDisplay -> Maybe ReverseDisplay - alterRevDisplay pkgid rev = keepMap $ updateReverseDisplay indexFunc updated (lookupVersions pkgid) rev - --------------------------------------------------------------------------------- --- Flattening the graph --- Exposing indirect dependencies is as simple as taking the set difference --- of the edges of a node in the dependency graph G and its closure G+. - --- Collect all indirect versioned dependencies. This takes around 45 seconds --- on the current package index (in ghci). It probably isn't worth exposing. -packageIdClosure :: RevDeps -> Map PackageId (Set PackageId) -packageIdClosure revs = Map.fromDistinctAscList $ transitiveClosure - (concatMap getNodes $ Map.toList revs) - (\pkg -> maybe [] (concatMap getEdges . Map.toList) - $ Map.lookup (packageVersion pkg) . fst =<< Map.lookup (packageName pkg) revs) - where - getNodes :: (PackageName, (Map Version a, b)) -> [PackageId] - getNodes (name, (versions, _)) = map (PackageIdentifier name) $ Map.keys versions - - getEdges :: (PackageName, Set Version) -> [PackageId] - getEdges (name, versions) = map (PackageIdentifier name) $ Set.toList versions - --- Collect all indirect name dependencies. This takes around 2 seconds on the --- current package index. It should be fine to reconstruct from scratch every --- time the package index is updated, since code to incrementally update a --- transitive closure can be messy and stateful and complicated. -packageNameClosure :: RevDeps -> Map PackageName (Set PackageName) -packageNameClosure revs = Map.fromDistinctAscList $ transitiveClosure - (Map.keys revs) - (\pkg -> maybe [] (concatMap Map.keys . Map.elems . fst) - $ Map.lookup pkg revs) - --- Get the transitive closure of a graph from the set of nodes and a neighbor --- function. This implementation uses depth-first search in the ST monad --- where cycles are broken if a visited node has been seen before. --- --- The same basic algorithm could be used to make a DAG structure. -transitiveClosure :: forall a. Ord a => [a] -> (a -> [a]) -> [(a, Set a)] -transitiveClosure core edges = runST $ do - list <- mapM (\node -> newSTRef Nothing >>= \ref -> return (node, ref)) core - let visited = Map.fromList list - mapM_ (collect visited) core - list' <- mapM (\(node, ref) -> readSTRef ref >>= \val -> return (node, val)) list - return [ (node, nodes) | (node, Just nodes) <- list' ] - where - collect :: Map a (STRef s (Maybe (Set a))) -> a -> ST s (Set a) - collect links node = do - case Map.lookup node links of - -- attempting to visit a node which wasn't given to us - Nothing -> return Set.empty - Just ref -> readSTRef ref >>= \t -> case t of - -- the node has already been visited - Just calc -> return calc - Nothing -> do - -- Mark the node as visited with results pending. If a cycle - -- brings us back to collect the same node, it will yield - -- an empty list. This breaks the cycle for whatever node - -- was visited first. - -- - -- There are smarter algorithms that can - -- get transitive closures of cyclic graphs, but cycles are - -- highly pathological for the types of graphs we'll be - -- traversing, so no need to worry. - writeSTRef ref $ Just Set.empty - let outEdges = edges node - collected <- fmap Set.unions $ mapM (collect links) outEdges - let connected = Set.union (Set.fromList outEdges) collected - writeSTRef ref $ Just connected - return connected ------------------------------------ Utility --- For cases when, if a Map or Set is empty, it's as good as nothing at all. -keepMap :: Ord k => Map k a -> Maybe (Map k a) -keepMap con = if Map.null con then Nothing else Just con - -keepMaps :: (Ord k, Ord k') => (Map k a, Map k' b) -> Maybe (Map k a, Map k' b) -keepMaps con@(c, c') = if Map.null c && Map.null c' then Nothing else Just con - -keepSet :: Ord a => Set a -> Maybe (Set a) -keepSet con = if Set.null con then Nothing else Just con - --------------------------------------------------------------------------------- --- State --- --- Last but agnostic of other ranking schemes, --- methods for manipulating the global state. - -deriveSafeCopy 0 'base ''ReverseIndex -deriveSafeCopy 0 'base ''ReverseCount - -initialReverseIndex :: ReverseIndex -initialReverseIndex = emptyReverseIndex - -getReverseIndex :: Query ReverseIndex ReverseIndex -getReverseIndex = ask - -replaceReverseIndex :: ReverseIndex -> Update ReverseIndex () -replaceReverseIndex = put - -addReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -addReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = addPackage pkgid deps revs - in put revs' >> return rec - -removeReversePackage :: PackageId -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -removeReversePackage pkgid deps = get >>= \revs -> - let (revs', rec) = removePackage pkgid deps revs - in put revs' >> return rec - -changeReversePackage :: PackageId -> CombinedDeps -> CombinedDeps -> Update ReverseIndex (Map PackageName [Version]) -changeReversePackage pkgid deps deps' = get >>= \revs -> - let (revs', rec) = changePackage pkgid deps deps' revs - in put revs' >> return rec - -getReverseCount :: PackageName -> Query ReverseIndex ReverseCount -getReverseCount pkg = asks $ Map.findWithDefault emptyReverseCount pkg . reverseCount - -getFlattenedReverse :: PackageName -> Query ReverseIndex (Set PackageName) -getFlattenedReverse pkg = asks $ Map.findWithDefault Set.empty pkg . flattenedReverse - -makeAcidic ''ReverseIndex ['getReverseIndex - ,'replaceReverseIndex - ,'addReversePackage - ,'removeReversePackage - ,'changeReversePackage - ,'getReverseCount - ,'getFlattenedReverse - ] +----------------------------Graph Utility---------- +suc :: RevDeps -> Vertex -> Set Vertex +suc g v = fromList $ g Arr.! v + +insEdges :: Int -> [(NodeId, [NodeId])] -> RevDeps -> RevDeps +insEdges nodesize edges revdeps = Arr.accumArray union [] (0, nodesize) (edges ++ Arr.assocs revdeps) + +-------------------------------------- + +instance (SafeCopy a, SafeCopy b, Ord a, Ord b) => SafeCopy (Bimap a b) where + getCopy = contain $ fmap Bimap.fromList safeGet + putCopy = contain . safePut . Bimap.toList + +-- instance (SafeCopy a, SafeCopy b) => SafeCopy (Graph) where +-- putCopy = contain . safePut . Gr.edges +-- getCopy = contain $ fmap (Gr.buildG (0,maxNodes)) safeGet + +$(deriveSafeCopy 0 'base ''ReverseIndex) +$(deriveSafeCopy 0 'base ''ReverseCount) + +getDependencies :: (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set PackageName) +getDependencies pkg revs = + names revs =<< getDependenciesRaw pkg revs + +getDependenciesRaw :: (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesRaw pkg (ReverseIndex revdeps nodemap) = do + enodeid <- try (looky pkg nodemap) + onRight enodeid $ \nodeid -> + nodeid `delete` suc revdeps nodeid + +-- | The flat/total/transitive/indirect reverse dependencies are all the packages that depend on something that depends on the given 'pkg' +getDependenciesFlat :: forall m. (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set PackageName) +getDependenciesFlat pkg revs = + names revs =<< getDependenciesFlatRaw pkg revs + +getDependenciesFlatRaw :: forall m. (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap) = do + enodeid <- try (looky pkg nodemap) + onRight enodeid $ \nodeid -> + nodeid `delete` fromList (Gr.reachable revdeps nodeid) + +-- | The direct dependencies depend on the given 'pkg' directly, i.e. not transitively +getDirectCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int +getDirectCount pkg revs = do + length <$> getDependenciesRaw pkg revs + +-- | Given a set of NodeIds, look up the package names for all of them +names :: MonadThrow m => ReverseIndex -> Set NodeId -> m (Set PackageName) +names (ReverseIndex _ nodemap) ids = do + fromList <$> mapM (`lookupR` nodemap) (toList ids) + +onRight :: Monad m => Either SomeException t -> (t -> Set NodeId) -> m (Set NodeId) +onRight e fun = do + case e of + Left (_ :: SomeException) -> do + pure mempty + Right nodeid -> + pure $ fun nodeid + +-- | The flat/total/transitive/indirect dependency count is the amount of package names that depend transitively on the given 'pkg' +getTotalCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int +getTotalCount pkg revs = do + length <$> getDependenciesFlatRaw pkg revs + +getReverseCount :: (HasCallStack, MonadCatch m) => PackageName -> ReverseIndex -> m (Int, Int) +getReverseCount pkg revs = do + direct <- getDirectCount pkg revs + total <- getTotalCount pkg revs + pure (direct, total) diff --git a/src/Distribution/Server/Framework/MemSize.hs b/src/Distribution/Server/Framework/MemSize.hs index 4af5d251f..d98e00085 100644 --- a/src/Distribution/Server/Framework/MemSize.hs +++ b/src/Distribution/Server/Framework/MemSize.hs @@ -17,6 +17,10 @@ import Data.Set (Set) import qualified Data.IntSet as IntSet import Data.IntSet (IntSet) import Data.Sequence (Seq) +import qualified Data.Bimap as Bimap +import Data.Bimap (Bimap) +import qualified Data.Graph as Gr +import Data.Graph (Graph) import qualified Data.Foldable as Foldable import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS @@ -223,6 +227,12 @@ instance MemSize e => MemSize (V.Vector e) where memSizeUVector :: V.U.Unbox e => Int -> V.U.Vector e -> Int memSizeUVector sz a = 5 + (V.U.length a * sz) `div` wordSize +instance (MemSize a, MemSize b) => MemSize (Bimap a b) where + memSize m = sum [ 6 + memSize k + memSize v | (k,v) <- Bimap.toList m ] + +instance MemSize Graph where + memSize m = sum [ 6 + memSize v | v <- Gr.edges m ] + ---- diff --git a/src/Distribution/Server/Pages/Reverse.hs b/src/Distribution/Server/Pages/Reverse.hs index 69a371c0a..8f57039f7 100644 --- a/src/Distribution/Server/Pages/Reverse.hs +++ b/src/Distribution/Server/Pages/Reverse.hs @@ -1,177 +1,215 @@ +{-# LANGUAGE NamedFieldPuns, RecordWildCards, NamedFieldPuns, BlockArguments #-} module Distribution.Server.Pages.Reverse ( - reversePackageRender, - reverseFlatRender, - reverseStatsRender, - reversePackagesRender, - reversePackageSummary + ReverseHtmlUtil(..) + , reverseHtmlUtil + , LatestOrOld(..) ) where import Distribution.Server.Features.ReverseDependencies -import Distribution.Server.Packages.Reverse -import Distribution.Server.Packages.Preferred +import Distribution.Server.Features.PreferredVersions + import Distribution.Package import Distribution.Text (display) import Distribution.Version +import Data.Function ((&)) import qualified Data.Map as Map +import Data.Set (Set, toList) import Text.XHtml.Strict -reversePackageRender :: PackageId -- ^ The package whose information is displayed. - -> (PackageId -> String) -- ^ Generating URIs for package pages. - -> ReverseResource -- ^ The resource for generating revdeps-related URIs. - -> Bool -- ^ Whether the ReverseDisplay was rendered for recent (True) or older (False) versions of the package. - -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. - -> [Html] -reversePackageRender pkgid packageLink r isRecent (ReversePageRender renders counts total) = - let packageAnchor = anchor ! [href $ packageLink pkgid] << display pkgid - hasVersion = packageVersion pkgid /= Version [] [] - pkgname = packageName pkgid - statLinks = paragraph << - [ toHtml "Check out the " - , anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions" - , toHtml $ " of " ++ display pkgid ++ " and its " - , anchor ! [href $ reverseAllUri r "" pkgname] << "indirect dependencies", toHtml "." ] - versionBox = if hasVersion && total /= allCounts - then thediv ! [theclass "notification"] << [toHtml $ "These statistics only apply to this version of " ++ display pkgname ++ ". See also ", anchor ! [href $ reverseNameUri r "" pkgname] << [toHtml "packages which depend on ", emphasize << "any", toHtml " version"], toHtml $ " (all " ++ show total ++ " of them)."] - else noHtml - allCounts = uncurry (+) counts - otherCount = case total - allCounts of - diff | diff > 0 -> paragraph << [show diff ++ " packages depend on versions of " ++ display pkgid ++ " other than this one."] - _ -> noHtml - (pageText, nonPageText) = (if isRecent then id else uncurry $ flip (,)) (recentText, nonRecentText) - otherLink = if isRecent then reverseOldUri r "" pkgid else reverseUri r "" pkgid - in h2 << (display pkgid ++ ": " ++ num allCounts "reverse dependencies" "reverse dependency"):versionBox:case counts of - (0, 0) -> - [ paragraph << [toHtml "No packages depend on ", - packageAnchor, toHtml "."] - ] - (0, count) -> - [ paragraph << [toHtml "No packages depend on ", - if hasVersion then noHtml else toHtml "some version of ", - packageAnchor, - toHtml $ pageText 0 ++ " However, ", - altVersions count nonPageText otherLink, - toHtml "."] - ] ++ [otherCount, statLinks] - (count, count') -> - [ (paragraph<<) $ [ mainVersions count pageText packageAnchor, toHtml " (listed below)." ] - ++ if count' > 0 then [ toHtml " Additionally, " - , altVersions count' nonPageText otherLink - , toHtml $ ". That's " ++ show (count+count') ++ " in total." ] - else [] - ] ++ (if isRecent then [] else [paragraph << oldText]) ++ [otherCount, statLinks, reverseTable] - where - mainVersions count textFunc pkgLink = toHtml - [ toHtml $ num count "packages depend on " "package depends on " - , pkgLink - , toHtml $ textFunc count - ] - altVersions count textFunc altLink = toHtml - [ anchor ! [href altLink] << num count "packages" "package" - , toHtml $ num' count " depend on " " depends on " ++ display pkgid ++ textFunc count - ] - recentText count = ' ':num' count "in their latest versions" "in its latest version" - nonRecentText count = ' ':num' count "only in older or deprecated versions" "only in an older or deprecated version" - oldText = "The latest version of each package below, which doesn't depend on " ++ display pkgid ++ ", is linked from the first column. The version linked from the second column is the one which has a dependency on " ++ display pkgid ++", but it's no longer the preferred installation candidate. Note that packages which depend on versions of " ++ display pkgid ++ " not uploaded to Hackage are treated as not depending on it at all." - - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ Version [] [] ] << display (packageName pkg) - , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseNameUri r "" $ packageName pkg] << "view", toHtml ")" ] ] - | (ReverseRender pkg status count, n) <- zip renders [(1::Int)..] ] - - renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] - renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] - renderStatus _ = [] - -reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseResource -> ReverseCount -> [(PackageName, Int)] -> [Html] -reverseFlatRender pkgname packageLink r (ReverseCount total flat _) pairs = - h2 << (display pkgname ++ ": " ++ num flat "total reverse dependencies" "reverse dependency"):case (total, flat) of - (0, 0) -> [paragraph << [toHtml "No packages depend on ", toPackage pkgname]] - _ -> - [ paragraph << if total == flat - then [toHtml "All packages which use ", toPackage pkgname, toHtml " depend on it ", anchor ! [href $ reverseNameUri r "" pkgname] << "directly", toHtml $ ". " ++ onlyPackage total] - else [toPackage pkgname, toHtml " has ", anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package", toHtml $ " which directly " ++ num' total "depend" "depends" ++ " on it, but there are more packages which depend on ", emphasize << "those", toHtml $ " packages. If you flatten the tree of reverse dependencies, you'll find " ++ show flat ++ " packages which use " ++ display pkgname ++ ", and " ++ show (flat-total) ++ " which do so without depending directly on it. All of these packages are listed below."] - , paragraph << [toHtml "See also the ", anchor ! [href $ reverseStatsUri r "" pkgname] << "statistics for specific versions", toHtml $ " of " ++ display pkgname ++ "."] - , reverseTable - ] - where - toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg +data LatestOrOld + = OnlyLatest + | OnlyOlder + deriving Eq - onlyPackage count = if count == 1 then "There's only one:" else "There are " ++ show count ++ ":" +data ReverseHtmlUtil = ReverseHtmlUtil { + reversePackageRender :: PackageId -> (PackageId -> String) -> LatestOrOld -> ReversePageRender -> [Html] + , reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + , reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + , reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + } - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total reverse dependencies" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << toPackage pkg - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseAllUri r "" pkg] << "view", toHtml ")" ] - ] - | ((pkg, count), n) <- zip pairs [(1::Int)..] ] - --- /package/:package/reverse/summary -reverseStatsRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseResource -> ReverseCount -> [Html] -reverseStatsRender pkgname allVersions packageLink r (ReverseCount total flat versions) = - h2 << (display pkgname ++ ": reverse dependency statistics"): - [ case total of - 0 -> paragraph << [ toHtml "No packages depend on ", thisPackage, toHtml "." ] - _ -> toHtml - [ paragraph << [ anchor ! [href $ reverseNameUri r "" pkgname] << num total "packages" "package" - , toHtml $ num' total " depend" " depends" - , toHtml " directly on ", thisPackage, toHtml "." ] - , paragraph << [ toHtml $ num (flat-total) "packages depend" "package depends" ++ " indirectly on " ++ display pkgname ++ "." ] - , paragraph << [ anchor ! [href $ reverseAllUri r "" pkgname] << num flat "packages" "package" - , toHtml $ num' flat " depend" " depends" ++ " on " ++ display pkgname ++ " in total." - ] - ] - , versionTable ] +reverseHtmlUtil :: ReverseFeature -> ReverseHtmlUtil +reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} where - toPackage pkgid = anchor ! [href $ packageLink pkgid] << display pkgid - thisPackage = toPackage (PackageIdentifier pkgname $ Version [] []) - - versionTable = thediv << table << versionTableRows - versionTableRows = - [ tr << [ th << "Version", th << "Reverse dependency count" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgid ] << display version - , td << [ toHtml $ show (Map.findWithDefault 0 version versions) ++ " (" - , anchor ! [href $ reverseUri r "" pkgid] << "view", toHtml ")" ] + reversePackageRender :: PackageId -- ^ The package whose information is displayed. + -> (PackageId -> String) -- ^ Generating URIs for package pages. + -> LatestOrOld -- ^ Whether the ReverseDisplay was rendered for recent (OnlyLatest) or older (OnlyOlder) versions of the package. + -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. + -> [Html] + reversePackageRender pkgid packageLink isRecent (ReversePageRender renders counts total) = + let packageAnchor = anchor ! [href $ packageLink pkgid] << display pkgid + hasVersion = packageVersion pkgid /= nullVersion + pkgname = packageName pkgid + statLinks = paragraph << + [ toHtml "Check out the " + , anchor ! [href $ reverseVerboseUri reverseResource "" pkgname] << "exhaustive listings for all versions" + , toHtml $ " of " ++ display pkgid ++ " and its " + , anchor ! [href $ reverseFlatUri reverseResource "" pkgname] << "indirect dependencies", toHtml "." ] + versionBox = if hasVersion && total /= allCounts + then thediv ! [theclass "notification"] << [toHtml $ "These statistics only apply to this version of " ++ display pkgname ++ ". See also ", anchor ! [href $ reverseNameUri reverseResource "" pkgname] << [toHtml "packages which depend on ", emphasize << "any", toHtml " version"], toHtml $ " (all " ++ show total ++ " of them)."] + else noHtml + allCounts = uncurry (+) counts + otherCount = case total - allCounts of + diff | diff > 0 -> paragraph << [show diff ++ " packages depend on versions of " ++ display (packageName pkgid) ++ " other than " ++ if hasVersion then display (packageVersion pkgid) else "the latest" ++ "."] + _ -> noHtml + (pageText, nonPageText) = (if isRecent == OnlyLatest then id else uncurry $ flip (,)) (recentText, nonRecentText) + otherLink = if isRecent == OnlyLatest then reverseOldUri reverseResource "" pkgid else reverseUri reverseResource "" pkgid + in h2 << (display pkgid ++ ": " ++ num allCounts "reverse dependencies" "reverse dependency"):versionBox:case counts of + (0, 0) -> + [ paragraph << [toHtml "No packages depend on ", + packageAnchor, toHtml "."] + ] + (0, count) -> + paragraph << [toHtml "No packages depend on ", + if hasVersion then noHtml else toHtml "some version of ", + packageAnchor, + toHtml $ pageText 0 ++ " However, ", + altVersions count nonPageText otherLink, + toHtml "."] : [otherCount, statLinks] + (count, count') -> + [ (paragraph<<) $ [ mainVersions count pageText packageAnchor, toHtml " (listed below)." ] + ++ if count' > 0 then [ toHtml " Additionally, " + , altVersions count' nonPageText otherLink + , toHtml $ ". That's " ++ show (count+count') ++ " in total." ] + else [] + ] ++ (if isRecent == OnlyLatest then [] else [paragraph << oldText]) ++ [otherCount, statLinks, reverseTable] + where + mainVersions count textFunc pkgLink = toHtml + [ toHtml $ num count "packages depend on " "package depends on " + , pkgLink + , toHtml $ textFunc count + ] + altVersions count textFunc altLink = toHtml + [ anchor ! [href altLink] << num count "packages" "package" + , toHtml $ num' count " depend on " " depends on " ++ display pkgid ++ textFunc count + ] + recentText count = ' ':num' count "in their latest versions" "in its latest version" + nonRecentText count = ' ':num' count "only in older or deprecated versions" "only in an older or deprecated version" + oldText = "The latest version of each package below, which doesn't depend on " ++ display pkgid ++ ", is linked from the first column. The version linked from the second column is the one which has a dependency on " ++ display pkgid ++", but it's no longer the preferred installation candidate. Note that packages which depend on versions of " ++ display pkgid ++ " not uploaded to Hackage are treated as not depending on it at all." + + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + tr ! [theclass "fancy"] << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ nullVersion ] << display (packageName pkg) + , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) + , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" $ packageName pkg] << "view", toHtml ")" ] ] + | (ReverseRender pkg status count, n) <- zip renders [(1::Int)..] ] + + renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] + renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] + renderStatus _ = [] + + reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] + reverseFlatRender pkgname packageLink (ReverseCount{totalCount, directCount}) pairs = + h2 << (display pkgname ++ ": " ++ num totalCount "total reverse dependencies" "reverse dependency"):case (directCount, totalCount) of + (0, 0) -> [paragraph << [toHtml "No packages depend on ", toPackage pkgname]] + _ -> + [ paragraph << if totalCount == directCount + then [ toHtml "All packages which use " + , toPackage pkgname + , toHtml " depend on it " + , anchor ! [href $ reverseNameUri reverseResource "" pkgname] << "directly" + , toHtml $ ". " ++ onlyPackage totalCount + ] + else [ toPackage pkgname + , toHtml " has " + , anchor ! [href $ reverseNameUri reverseResource "" pkgname] << num directCount "packages" "package" + , toHtml $ " which directly " ++ num' directCount "depend" "depends" ++ " on it, but there are more packages which depend on " + , emphasize << "those" + , toHtml $ " packages. If you flatten the tree of reverse dependencies, you'll find " + ++ show totalCount ++ " packages which use " ++ display pkgname ++ ", and " + ++ show (totalCount-directCount) ++ " which do so without depending directly on it. All of these packages are listed below." + ] + , paragraph << [toHtml "See also the ", anchor ! [href $ reverseVerboseUri reverseResource "" pkgname] << "exhaustive listings for all versions", toHtml $ " of " ++ display pkgname ++ "."] + , reverseTable + ] + where + toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg + + onlyPackage count = if count == 1 then "There's only one:" else "There are " ++ show count ++ ":" + + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << toPackage pkg + , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" pkg] << "view", toHtml ")" ] + ] + | ((pkg, count), n) <- zip pairs [(1::Int)..] ] + + -- /package/:package/reverse/verbose + reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] + reverseVerboseRender pkgname allVersions packageLink (ReverseCount {totalCount, directCount}) versions = + h2 << (display pkgname ++ ": reverse dependency statistics"): + [ case directCount of + 0 -> paragraph << [ toHtml "No packages depend on ", thisPackage, toHtml "." ] + _ -> toHtml + [ paragraph << [ anchor ! [href $ reverseNameUri reverseResource "" pkgname] << num directCount "packages" "package" + , toHtml $ num' directCount " depend" " depends" + , toHtml " directly on ", thisPackage, toHtml "." ] + , paragraph << [ toHtml $ num (totalCount - directCount) "packages depend" "package depends" ++ " indirectly on " ++ display pkgname ++ "." ] + , paragraph << [ anchor ! [href $ reverseFlatUri reverseResource "" pkgname] << num totalCount "packages" "package" + , toHtml $ num' totalCount " depend" " depends" ++ " on " ++ display pkgname ++ " in total." + ] + ] + , versionTable + , if length allVersions > limitVersions + -- Why the oldest? Such that the package can be cached indefinitely without having to get invalidated. + then thediv << [toHtml "Only showing the oldest ", toHtml (display limitVersions), toHtml " versions."] + else mempty + ] + + where + limitVersions = 10 + toPackage pkgid = anchor ! [href $ packageLink pkgid] << display pkgid + thisPackage = toPackage (PackageIdentifier pkgname $ nullVersion) + + versionTable = thediv << (table ! [theclass "fancy"]) << versionTableRows + versionTableRows = + (tr << [ th << "Version", th << "Reverse dependencies" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgid ] << display version + , td << [ row ] + ] + | (version, n) <- take limitVersions $ zip allVersions [(1::Int)..] + , let + pkgid = PackageIdentifier pkgname version + mkListOfLinks :: Set PackageIdentifier -> [Html] + mkListOfLinks pkgIdSet = + toList pkgIdSet & + map + \revDepPkgId -> + li << + anchor ! [href $ packageLink revDepPkgId] << display revDepPkgId + row :: Html + row = + Map.lookup version versions & + maybe + (toHtml "This version has no reverse dependencies.") + ((ulist <<) . mkListOfLinks) ] - | (version, n) <- zip allVersions [(1::Int)..], let pkgid = PackageIdentifier pkgname version ] - -num, num' :: Int -> String -> String -> String -num n plural singular = show n ++ " " ++ num' n plural singular -num' n plural singular = if n == 1 then singular else plural - --- /packages/reverse -reversePackagesRender :: (PackageName -> String) -> ReverseResource -> Int -> [(PackageName, Int, Int)] -> [Html] -reversePackagesRender packageLink r pkgCount triples = - h2 << "Reverse dependencies" : - [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] - , reverseTable ] - where - reverseTable = thediv << table << reverseTableRows - reverseTableRows = - [ tr << [ th << "Package name", th << "Total", th << "Direct" ] ] ++ - [ tr ! [theclass (if odd n then "odd" else "even")] << - [ td << anchor ! [href $ packageLink pkgname ] << display pkgname - , td << [ toHtml $ show flat ++ " (", anchor ! [href $ reverseStatsUri r "" pkgname ] << "view", toHtml ")" ] - , td << [ toHtml $ show total ++ " (", anchor ! [href $ reverseNameUri r "" pkgname ] << "view", toHtml ")" ] - ] -- and, indirect is flat-total, if those are ever explicitly served - | ((pkgname, total, flat), n) <- zip triples [(1::Int)..], flat /= 0 ] - -reversePackageSummary :: PackageId -> ReverseResource -> (Int, Int) -> (String, Html) -reversePackageSummary pkgid r (direct, version) = (,) "Reverse dependencies" $ - if direct == 0 - then toHtml "None" - else toHtml [ anchor ! [href $ reverseUri r "" pkgid ] << show version - , toHtml " for ", toHtml . display $ packageVersion pkgid - , toHtml " and " - , anchor ! [href $ reverseNameUri r "" (packageName pkgid) ] << show direct - , toHtml " total"] + num, num' :: Int -> String -> String -> String + num n plural singular = show n ++ " " ++ num' n plural singular + num' n plural singular = if n == 1 then singular else plural + + -- /packages/reverse + reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] + reversePackagesRender packageLink pkgCount namesWithCounts = + h2 << "Reverse dependencies" : + [ paragraph << [ "Hackage has " ++ show pkgCount ++ " packages. Here are all the packages that have package that depend on them:"] + , reverseTable ] + where + reverseTable = thediv << table << reverseTableRows + reverseTableRows = + (tr ! [theclass "fancy"] << [ th << "Package name", th << "Total", th << "Direct" ]) : + [ tr ! [theclass (if odd n then "odd" else "even")] << + [ td << anchor ! [href $ packageLink pkgname ] << display pkgname + , td << [ toHtml $ show totalCount ++ " (", anchor ! [href $ reverseVerboseUri reverseResource "" pkgname ] << "view", toHtml ")" ] + , td << [ toHtml $ show directCount ++ " (", anchor ! [href $ reverseNameUri reverseResource "" pkgname ] << "view", toHtml ")" ] + ] -- and, indirect is total-direct, if those are ever explicitly served + | ((pkgname, ReverseCount{directCount, totalCount}), n) <- zip namesWithCounts [(1::Int)..], totalCount /= 0 ] diff --git a/tests/RevDepCommon.hs b/tests/RevDepCommon.hs new file mode 100644 index 000000000..93c27d633 --- /dev/null +++ b/tests/RevDepCommon.hs @@ -0,0 +1,61 @@ +{-# LANGUAGE OverloadedStrings, NamedFieldPuns #-} +{-# LANGUAGE GeneralizedNewtypeDeriving, DerivingStrategies, DeriveGeneric #-} +module RevDepCommon where + +import GHC.Generics (Generic) +import Distribution.Server.Users.Types (UserId(..)) +import Data.List (intercalate) +import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) +import Distribution.Server.Packages.Types (PkgInfo(..)) +import qualified Data.Vector as Vector +import Distribution.Server.Packages.Types (CabalFileText(..)) +import qualified Data.ByteString.Lazy as BSL +import qualified Data.ByteString.Char8 as Char8 +import Data.Time (UTCTime(..), fromGregorian) +import Distribution.Version (mkVersion, versionNumbers) + +data Package b = + Package + { name :: b + , version :: Int + , deps :: [ b ] + } + deriving (Ord, Show, Eq) + +packToPkgInfo :: Show b => Package b -> PkgInfo +packToPkgInfo Package {name, version, deps} = + mkPackage (mkPackageName $ show name) [version] (depsToBS deps) + +mkPackage :: PackageName -> [Int] -> BSL.ByteString -> PkgInfo +mkPackage name intVersion depends = + let + version = mkVersion intVersion + -- e.g. "2.3" for [2,3] + dotVersion :: BSL.ByteString + dotVersion = BSL.fromStrict . Char8.intercalate "." . map (Char8.pack . show) $ versionNumbers version + cabalFilePrefix :: BSL.ByteString + cabalFilePrefix = "\ +\name: " <> BSL.fromStrict (Char8.pack $ unPackageName name) <> "\n\ +\version: " <> dotVersion <> "\n" + cabalFile :: CabalFileText + cabalFile = CabalFileText $ cabalFilePrefix <> if depends /= "" then "library\n build-depends: " <> depends else "" + in + PkgInfo + (PackageIdentifier name version) + (Vector.fromList [(cabalFile, (UTCTime (fromGregorian 2020 1 1) 0, UserId 1))]) + mempty + +depsToBS :: Show b => [ b ] -> BSL.ByteString +depsToBS deps = + let + depsBS :: String + depsBS = intercalate "," $ map show deps + in BSL.fromStrict $ Char8.pack $ depsBS + + +newtype TestPackage = TestPackage Word + deriving stock Generic + deriving newtype (Bounded, Enum, Eq, Ord) + +instance Show TestPackage where + show (TestPackage word) = "package" <> show word diff --git a/tests/ReverseDependenciesTest.hs b/tests/ReverseDependenciesTest.hs new file mode 100644 index 000000000..437e88e16 --- /dev/null +++ b/tests/ReverseDependenciesTest.hs @@ -0,0 +1,219 @@ +{-# LANGUAGE OverloadedStrings, NamedFieldPuns, TypeApplications, ScopedTypeVariables #-} +module Main where + +import Control.Monad (foldM) +import Control.Monad.Catch (MonadCatch, SomeException, catch) +import Control.Monad.IO.Class (MonadIO, liftIO) +import qualified Data.Array as Arr +import qualified Data.Bimap as Bimap +import Data.Foldable (for_) +import Data.List (partition) +import qualified Data.Map as Map +import qualified Data.Set as Set + +import Distribution.Package (mkPackageName, packageId, packageName) +import Distribution.Server.Features.PreferredVersions.State (PreferredVersions(..), VersionStatus(NormalVersion), PreferredInfo(..)) +import Distribution.Server.Features.ReverseDependencies (ReverseFeature(..), ReverseCount(..), reverseFeature) +import Distribution.Server.Features.ReverseDependencies.State (ReverseIndex(..), addPackage, constructReverseIndex, emptyReverseIndex, getDependenciesFlat, getDependencies, getDependenciesFlatRaw, getDependenciesRaw) +import Distribution.Server.Framework.Hook (newHook) +import Distribution.Server.Framework.MemState (newMemStateWHNF) +import Distribution.Server.Packages.PackageIndex as PackageIndex +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Version (mkVersion, version0) + +import Test.Tasty (TestTree, defaultMain, testGroup) +import Test.Tasty.HUnit + +import qualified Hedgehog.Range as Range +import qualified Hedgehog.Gen as Gen +import Hedgehog ((===), Group(Group), MonadGen, MonadTest, Property, PropertyT, checkSequential, failure, footnoteShow, forAll, property) + +import RevDepCommon (Package(..), TestPackage(..), mkPackage, packToPkgInfo) + +mtlBeelineLens :: [PkgInfo] +mtlBeelineLens = + [ mkPackage "base" [4,15] "" + , mkPackage "mtl" [2,3] "base" + -- Note that this example is a bit unrealistic + -- since these two do not depend on base... + , mkPackage "beeline" [0] "mtl" + , mkPackage "lens" [0] "mtl" + ] + +mkRevFeat :: [PkgInfo] -> IO ReverseFeature +mkRevFeat pkgs = do + let + idx = PackageIndex.fromList pkgs + preferredVersions = + PreferredVersions + { preferredMap = mempty + , deprecatedMap = mempty + , migratedEphemeralPrefs = False + } + updateReverse <- newHook + Right constructed <- pure $ constructReverseIndex idx + memState <- newMemStateWHNF constructed + pure $ + reverseFeature + (pure idx) + (pure preferredVersions) + memState + updateReverse + +allTests :: TestTree +allTests = testGroup "ReverseDependenciesTest" + [ testCase "with set [beeline->mtl] and querying for mtl, we get beeline" $ do + let pkgs = + [ mkPackage "mtl" [2,3] "base" + , mkPackage "beeline" [0] "mtl" + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (version0, Just NormalVersion))] + assertEqual "reverse dependencies must be [beeline]" ref res + , testCase "revPackageName selects only the version with an actual dependency, even if it is not the newest" $ do + let pkgs = + [ mkPackage "mtl" [2,3] "base" + , mkPackage "mtl-tf" [9000] "base" + , mkPackage "beeline" [0] "mtl" + , mkPackage "beeline" [1] "mtl-tf" + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageId does select old version when queried with old reverse dependency" $ do + let mtl = mkPackage "mtl" [2,3] "base" + pkgs = + [ mtl + , mkPackage "mtl-tf" [9000] "base" + , mkPackage "beeline" [0] "mtl" + , mkPackage "beeline" [1] "mtl-tf" + ] + ReverseFeature{revPackageId} <- mkRevFeat pkgs + res <- revPackageId (packageId mtl) + -- Nothing because it is not the 'best version' + let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] + assertEqual "reverse dependencies must be [beeline v0]" ref res + , testCase "revPackageName can find multiple packages" $ do + let pkgs = + [ mkPackage "mtl" [2,3] "base" + , mkPackage "beeline" [0] "mtl" + , mkPackage "mario" [0] "mtl" + ] + ReverseFeature{revPackageName} <- mkRevFeat pkgs + res <- revPackageName "mtl" + let ref = Map.fromList [ ("beeline", (mkVersion [0], Just NormalVersion)) + , ("mario", (mkVersion [0], Just NormalVersion)) + ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageFlat 'base' finds [beeline, lens, mtl]" $ do + ReverseFeature{revPackageFlat} <- mkRevFeat mtlBeelineLens + res <- revPackageFlat "base" + let ref = [ ("beeline", 0), ("lens", 0), ("mtl", 2) ] + assertEqual "reverse dependencies must be [beeline v0, mario v0]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revPackageStats 'base' returns 1,3" $ do + ReverseFeature{revPackageStats} <- mkRevFeat mtlBeelineLens + res <- revPackageStats "base" + let ref = ReverseCount { directCount = 1, totalCount = 3 } + assertEqual "must be directCount=1, totalCount=3" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], queryReverseDeps 'base' returns [mtl],[beeline,lens]" $ do + ReverseFeature{queryReverseDeps} <- mkRevFeat mtlBeelineLens + res <- queryReverseDeps "base" + let ref = (["mtl"], ["beeline", "lens"]) + assertEqual "must be direct=[mtl], indirect=[beeline,lens]" ref res + , testCase "with set [beeline->mtl->base, lens->mtl->base], revCountForAllPackages returns [(base,1,3),(mtl,2,2),(beeline,0,0),(lens,0,0)]" $ do + ReverseFeature{revCountForAllPackages} <- mkRevFeat mtlBeelineLens + res <- revCountForAllPackages + let ref = [("beeline",ReverseCount 0 0),("lens",ReverseCount 0 0),("base",ReverseCount 1 3),("mtl",ReverseCount 2 2)] + assertEqual "must match reference" ref res + , testCase "revDisplayInfo" $ do + ReverseFeature{revDisplayInfo} <- mkRevFeat mtlBeelineLens + res <- revDisplayInfo + assertEqual "beeline preferred is old" (PreferredInfo [] [] Nothing, [mkVersion [0]]) (res "beeline") + , testCase "hedgehogTests" $ do + res <- hedgehogTests + assertEqual "hedgehog test pass" True res + ] + +genPacks :: PropertyT IO [Package TestPackage] +genPacks = do + numPacks <- forAll $ Gen.int (Range.linear 1 10) + allowMultipleVersions <- forAll Gen.bool -- remember that this shrinks to False + packs <- forAll $ packsUntil allowMultipleVersions numPacks mempty + pure packs + +prop_constructRevDeps :: Property +prop_constructRevDeps = property $ do + packs <- genPacks + let idx = PackageIndex.fromList $ map packToPkgInfo packs + ReverseIndex foldedDeps foldedMap <- foldM (packageFolder @_ @TestPackage) emptyReverseIndex packs + Right (ReverseIndex constructedDeps constructedMap) <- pure $ constructReverseIndex idx + for_ (PackageIndex.allPackageNames idx) $ \name -> do + foundFolded :: Int <- Bimap.lookup name foldedMap + foundConstructed :: Int <- Bimap.lookup name constructedMap + -- they are not nessarily equal, since they may have been added in a different order! + -- so this doesn't necessarily hold: + -- foundFolded === foundConstructed + + -- but they should have the same deps + foldedPackNames <- mapM (`Bimap.lookupR` foldedMap) (foldedDeps Arr.! foundFolded) + constructedPackNames <- mapM (`Bimap.lookupR` constructedMap) (constructedDeps Arr.! foundConstructed) + Set.fromList foldedPackNames === Set.fromList constructedPackNames + +prop_statsEqualsDeps :: Property +prop_statsEqualsDeps = property $ do + packs <- genPacks + let packages = map packToPkgInfo packs + Right revs <- pure $ constructReverseIndex $ PackageIndex.fromList packages + pkginfo <- forAll $ Gen.element packages + let name = packageName pkginfo + directSet <- getDependenciesRaw name revs + totalSet <- getDependenciesFlatRaw name revs + directNames <- getDependencies name revs + totalNames <- getDependenciesFlat name revs + length directSet === length directNames + length totalSet === length totalNames + +packageFolder :: (MonadCatch m, MonadIO m, MonadTest m, Show b) => ReverseIndex -> Package b -> m ReverseIndex +packageFolder index pkg@(Package name _version deps) = + catch (liftIO $ addPackage (mkPackageName $ show name) (map (mkPackageName . show) deps) index) + $ \(e :: SomeException) -> do + footnoteShow pkg + footnoteShow index + footnoteShow e + failure + + +genPackage :: forall m b. (MonadGen m, Enum b, Bounded b, Ord b) => b -> [Package b] -> m (Package b) +genPackage newName available = do + version <- Gen.int (Range.linear 0 10) + depPacks :: [Package b] <- Gen.subsequence available + pure $ Package {name = newName, version, deps = map name depPacks } + +packsUntil :: forall m b. (Ord b, Bounded b, MonadGen m, Enum b) => Bool -> Int -> [Package b] -> m [Package b] +packsUntil allowMultipleVersions limit generated | length generated < limit = do + makeNewPack <- Gen.bool -- if not new pack, just make a new version of an existing + toInsert <- + if makeNewPack || generated == mempty || not allowMultipleVersions + then + genPackage (toEnum $ length generated) generated + else do + Package { name = prevName } <- Gen.element generated + let (prevNamePacks, nonPrevName) = partition ((== prevName) . name) generated + depPacks :: [Package b] <- Gen.subsequence nonPrevName + let newVersion = 1 + maximum (map version prevNamePacks) + pure $ Package {name = prevName, version = newVersion, deps = map name depPacks} + let added = generated ++ [toInsert] + packsUntil allowMultipleVersions limit added +packsUntil _ _ generated = pure generated + +hedgehogTests :: IO Bool +hedgehogTests = + checkSequential $ Group "hedgehogTests" + [ ("prop_constructRevDeps", prop_constructRevDeps) + , ("prop_statsEqualsDeps", prop_statsEqualsDeps) + ] + +main :: IO () +main = defaultMain allTests From b168fb6aed3d0dc383cb78a17cd9006d9f100483 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Tue, 31 May 2022 21:43:46 -0500 Subject: [PATCH 2/6] Use tables instead of generating prose --- src/Distribution/Server/Pages/Reverse.hs | 116 +++++------------------ 1 file changed, 24 insertions(+), 92 deletions(-) diff --git a/src/Distribution/Server/Pages/Reverse.hs b/src/Distribution/Server/Pages/Reverse.hs index 8f57039f7..cffff8181 100644 --- a/src/Distribution/Server/Pages/Reverse.hs +++ b/src/Distribution/Server/Pages/Reverse.hs @@ -39,57 +39,19 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} -> LatestOrOld -- ^ Whether the ReverseDisplay was rendered for recent (OnlyLatest) or older (OnlyOlder) versions of the package. -> ReversePageRender -- ^ Obtained from a ReverseDisplay-rendering function. -> [Html] - reversePackageRender pkgid packageLink isRecent (ReversePageRender renders counts total) = - let packageAnchor = anchor ! [href $ packageLink pkgid] << display pkgid - hasVersion = packageVersion pkgid /= nullVersion - pkgname = packageName pkgid - statLinks = paragraph << - [ toHtml "Check out the " - , anchor ! [href $ reverseVerboseUri reverseResource "" pkgname] << "exhaustive listings for all versions" - , toHtml $ " of " ++ display pkgid ++ " and its " - , anchor ! [href $ reverseFlatUri reverseResource "" pkgname] << "indirect dependencies", toHtml "." ] - versionBox = if hasVersion && total /= allCounts - then thediv ! [theclass "notification"] << [toHtml $ "These statistics only apply to this version of " ++ display pkgname ++ ". See also ", anchor ! [href $ reverseNameUri reverseResource "" pkgname] << [toHtml "packages which depend on ", emphasize << "any", toHtml " version"], toHtml $ " (all " ++ show total ++ " of them)."] - else noHtml - allCounts = uncurry (+) counts - otherCount = case total - allCounts of - diff | diff > 0 -> paragraph << [show diff ++ " packages depend on versions of " ++ display (packageName pkgid) ++ " other than " ++ if hasVersion then display (packageVersion pkgid) else "the latest" ++ "."] - _ -> noHtml - (pageText, nonPageText) = (if isRecent == OnlyLatest then id else uncurry $ flip (,)) (recentText, nonRecentText) - otherLink = if isRecent == OnlyLatest then reverseOldUri reverseResource "" pkgid else reverseUri reverseResource "" pkgid - in h2 << (display pkgid ++ ": " ++ num allCounts "reverse dependencies" "reverse dependency"):versionBox:case counts of - (0, 0) -> - [ paragraph << [toHtml "No packages depend on ", - packageAnchor, toHtml "."] + reversePackageRender pkgid packageLink isRecent (ReversePageRender renders (count, count') total) = + let allCounts = count + count' + firstTh = toHtml ("Depend on the " <> (if packageVersion pkgid == nullVersion then "latest" else "given") <> " version") + in h2 << (display pkgid ++ ": " ++ display allCounts ++ " reverse dependencies"): + [ if isRecent == OnlyLatest + then toHtml "No version specified, so showing reverse dependencies for latest version." + else toHtml "" + , table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << toHtml "Depend on other versions", th << toHtml "Total" ] + , tr << [ td << toHtml (display count), td << toHtml (display count'), td << toHtml (display total) ]] + , reverseTable ] - (0, count) -> - paragraph << [toHtml "No packages depend on ", - if hasVersion then noHtml else toHtml "some version of ", - packageAnchor, - toHtml $ pageText 0 ++ " However, ", - altVersions count nonPageText otherLink, - toHtml "."] : [otherCount, statLinks] - (count, count') -> - [ (paragraph<<) $ [ mainVersions count pageText packageAnchor, toHtml " (listed below)." ] - ++ if count' > 0 then [ toHtml " Additionally, " - , altVersions count' nonPageText otherLink - , toHtml $ ". That's " ++ show (count+count') ++ " in total." ] - else [] - ] ++ (if isRecent == OnlyLatest then [] else [paragraph << oldText]) ++ [otherCount, statLinks, reverseTable] where - mainVersions count textFunc pkgLink = toHtml - [ toHtml $ num count "packages depend on " "package depends on " - , pkgLink - , toHtml $ textFunc count - ] - altVersions count textFunc altLink = toHtml - [ anchor ! [href altLink] << num count "packages" "package" - , toHtml $ num' count " depend on " " depends on " ++ display pkgid ++ textFunc count - ] - recentText count = ' ':num' count "in their latest versions" "in its latest version" - nonRecentText count = ' ':num' count "only in older or deprecated versions" "only in an older or deprecated version" - oldText = "The latest version of each package below, which doesn't depend on " ++ display pkgid ++ ", is linked from the first column. The version linked from the second column is the one which has a dependency on " ++ display pkgid ++", but it's no longer the preferred installation candidate. Note that packages which depend on versions of " ++ display pkgid ++ " not uploaded to Hackage are treated as not depending on it at all." - reverseTable = thediv << table << reverseTableRows reverseTableRows = tr ! [theclass "fancy"] << [ th << "Package name", th << "Version", th << "Reverse dependencies" ] : @@ -103,34 +65,20 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] renderStatus _ = [] + renderCount ReverseCount{totalCount, directCount} = + table ! [ theclass "fancy" ] + << [ tr << [ th << firstTh, th << secondTh, th << toHtml "Total" ] + , tr << [ td << toHtml (display directCount), td << toHtml (display (totalCount - directCount)), td << toHtml (display totalCount) ]] + where + firstTh = toHtml "Direct reverse dependencies" + secondTh = toHtml "Indirect reverse dependencies" + reverseFlatRender :: PackageName -> (PackageName -> String) -> ReverseCount -> [(PackageName, Int)] -> [Html] - reverseFlatRender pkgname packageLink (ReverseCount{totalCount, directCount}) pairs = - h2 << (display pkgname ++ ": " ++ num totalCount "total reverse dependencies" "reverse dependency"):case (directCount, totalCount) of - (0, 0) -> [paragraph << [toHtml "No packages depend on ", toPackage pkgname]] - _ -> - [ paragraph << if totalCount == directCount - then [ toHtml "All packages which use " - , toPackage pkgname - , toHtml " depend on it " - , anchor ! [href $ reverseNameUri reverseResource "" pkgname] << "directly" - , toHtml $ ". " ++ onlyPackage totalCount - ] - else [ toPackage pkgname - , toHtml " has " - , anchor ! [href $ reverseNameUri reverseResource "" pkgname] << num directCount "packages" "package" - , toHtml $ " which directly " ++ num' directCount "depend" "depends" ++ " on it, but there are more packages which depend on " - , emphasize << "those" - , toHtml $ " packages. If you flatten the tree of reverse dependencies, you'll find " - ++ show totalCount ++ " packages which use " ++ display pkgname ++ ", and " - ++ show (totalCount-directCount) ++ " which do so without depending directly on it. All of these packages are listed below." - ] - , paragraph << [toHtml "See also the ", anchor ! [href $ reverseVerboseUri reverseResource "" pkgname] << "exhaustive listings for all versions", toHtml $ " of " ++ display pkgname ++ "."] - , reverseTable - ] + reverseFlatRender pkgname packageLink revCount pairs = + h2 << (display pkgname ++ ": " ++ "total reverse dependencies"):renderCount revCount:[reverseTable] where - toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg - onlyPackage count = if count == 1 then "There's only one:" else "There are " ++ show count ++ ":" + toPackage pkg = anchor ! [href $ packageLink pkg] << display pkg reverseTable = thediv << table << reverseTableRows reverseTableRows = @@ -143,19 +91,9 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} -- /package/:package/reverse/verbose reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] - reverseVerboseRender pkgname allVersions packageLink (ReverseCount {totalCount, directCount}) versions = + reverseVerboseRender pkgname allVersions packageLink revCount versions = h2 << (display pkgname ++ ": reverse dependency statistics"): - [ case directCount of - 0 -> paragraph << [ toHtml "No packages depend on ", thisPackage, toHtml "." ] - _ -> toHtml - [ paragraph << [ anchor ! [href $ reverseNameUri reverseResource "" pkgname] << num directCount "packages" "package" - , toHtml $ num' directCount " depend" " depends" - , toHtml " directly on ", thisPackage, toHtml "." ] - , paragraph << [ toHtml $ num (totalCount - directCount) "packages depend" "package depends" ++ " indirectly on " ++ display pkgname ++ "." ] - , paragraph << [ anchor ! [href $ reverseFlatUri reverseResource "" pkgname] << num totalCount "packages" "package" - , toHtml $ num' totalCount " depend" " depends" ++ " on " ++ display pkgname ++ " in total." - ] - ] + [ renderCount revCount , versionTable , if length allVersions > limitVersions -- Why the oldest? Such that the package can be cached indefinitely without having to get invalidated. @@ -165,8 +103,6 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} where limitVersions = 10 - toPackage pkgid = anchor ! [href $ packageLink pkgid] << display pkgid - thisPackage = toPackage (PackageIdentifier pkgname $ nullVersion) versionTable = thediv << (table ! [theclass "fancy"]) << versionTableRows versionTableRows = @@ -193,10 +129,6 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} ((ulist <<) . mkListOfLinks) ] - num, num' :: Int -> String -> String -> String - num n plural singular = show n ++ " " ++ num' n plural singular - num' n plural singular = if n == 1 then singular else plural - -- /packages/reverse reversePackagesRender :: (PackageName -> String) -> Int -> [(PackageName, ReverseCount)] -> [Html] reversePackagesRender packageLink pkgCount namesWithCounts = From 7f41c8b4fd590a872a3276020f880f2089c827b3 Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Sat, 11 Jun 2022 14:26:24 +0200 Subject: [PATCH 3/6] Speed up verbose revdep endpoint, clean up code --- benchmarks/RevDeps.hs | 12 +- exes/ReverseDepsForPackage.hs | 15 -- exes/ReverseDepsPreferredForPackage.hs | 38 ---- hackage-server.cabal | 10 - .../Server/Features/PackageList.hs | 40 ++-- .../Server/Features/ReverseDependencies.hs | 38 ++-- .../Features/ReverseDependencies/State.hs | 174 +++++++++--------- .../Server/Packages/PackageIndex.hs | 7 +- tests/RevDepCommon.hs | 24 +-- tests/ReverseDependenciesTest.hs | 70 +++---- 10 files changed, 186 insertions(+), 242 deletions(-) delete mode 100644 exes/ReverseDepsForPackage.hs delete mode 100644 exes/ReverseDepsPreferredForPackage.hs diff --git a/benchmarks/RevDeps.hs b/benchmarks/RevDeps.hs index 1548d22dc..25e3222c3 100644 --- a/benchmarks/RevDeps.hs +++ b/benchmarks/RevDeps.hs @@ -31,8 +31,8 @@ randomPacks gen limit generated | length generated < limit = do <*> pure mempty else do prevIdx <- uniformRM (0, length generated - 1) gen - let Package { name = prevName } = generated Vector.! prevIdx - (prevNamePacks, nonPrevName) = Vector.partition ((== prevName) . name) generated + let Package { pName = prevName } = generated Vector.! prevIdx + (prevNamePacks, nonPrevName) = Vector.partition ((== prevName) . pName) generated depPacks <- if mempty /= nonPrevName then do @@ -46,13 +46,13 @@ randomPacks gen limit generated | length generated < limit = do let newVersion = if mempty /= prevNamePacks - then 1 + maximum (fmap version prevNamePacks) + then 1 + maximum (fmap pVersion prevNamePacks) else 0 pure $ Package - { name = prevName - , version = newVersion - , deps = map name depPacks + { pName = prevName + , pVersion = newVersion + , pDeps = map pName depPacks } let added = generated <> pure toInsert randomPacks gen limit added diff --git a/exes/ReverseDepsForPackage.hs b/exes/ReverseDepsForPackage.hs deleted file mode 100644 index 004a3294e..000000000 --- a/exes/ReverseDepsForPackage.hs +++ /dev/null @@ -1,15 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -module Main where - -import Distribution.Server.Features.Core (packagesStateComponent) -import Distribution.Server.Features.Core.State (packageIndex) -import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getReverseCount) -import Distribution.Server.Framework.Feature (getState) - -main :: IO () -main = do - component <- packagesStateComponent maxBound False "state" - st <- getState component - let idx = packageIndex st - revIdx <- constructReverseIndex idx - print =<< getReverseCount "ADPfusion" revIdx diff --git a/exes/ReverseDepsPreferredForPackage.hs b/exes/ReverseDepsPreferredForPackage.hs deleted file mode 100644 index 7c9ec7d59..000000000 --- a/exes/ReverseDepsPreferredForPackage.hs +++ /dev/null @@ -1,38 +0,0 @@ -{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} -module Main where - -import System.Exit (die) -import Data.Maybe (isJust) -import Data.List (find) -import Control.Monad (when) -import Data.Map (toList) -import Distribution.Package -import Distribution.Version -import Distribution.Server.Features.Core (packagesStateComponent) -import Distribution.Server.Features.Core.State (packageIndex) -import Distribution.Server.Features.ReverseDependencies.State (constructReverseIndex, getDisplayInfo, perPackageReverse, dependsOnPkg) -import Distribution.Server.Features.PreferredVersions -import Distribution.Server.Features.PreferredVersions.State -import Distribution.Server.Framework.Feature (getState, queryState) - -main :: IO () -main = do - component <- packagesStateComponent maxBound False "state" - st <- getState component - let idx = packageIndex st - revIdx <- constructReverseIndex idx - - prefState <- preferredStateComponent False "state" - prefVersions <- queryState prefState GetPreferredVersions - let indexFunc = getDisplayInfo prefVersions idx - print $ indexFunc "ghc-prim" - print $ indexFunc "containers" - revDeps <- perPackageReverse indexFunc idx revIdx "ghc-prim" - -- Newer versions of 'containers' stopped depending on ghc-prim. So we expect 'containers' to not show up here. - print (toList revDeps) - print (dependsOnPkg idx (PackageIdentifier "ghc-prim" (mkVersion [0,6,0,1])) "containers") - let found = isJust $ find (== ("containers", (mkVersion [0,6,0,1], Nothing))) (toList revDeps) - when (not found) $ do - print revDeps - die "error!" - diff --git a/hackage-server.cabal b/hackage-server.cabal index 7a00fc78e..e96738517 100644 --- a/hackage-server.cabal +++ b/hackage-server.cabal @@ -630,13 +630,3 @@ test-suite DocTests , lib-server , doctest-parallel ^>= 0.2.2 -- doctest-parallel-0.2.2 is the first to filter out autogen-modules - -executable ReverseDepsForPackage - import: exe-defaults - - main-is: ReverseDepsForPackage.hs - -executable ReverseDepsPreferredForPackage - import: exe-defaults - - main-is: ReverseDepsPreferredForPackage.hs diff --git a/src/Distribution/Server/Features/PackageList.hs b/src/Distribution/Server/Features/PackageList.hs index ffea264e6..4589063af 100644 --- a/src/Distribution/Server/Features/PackageList.hs +++ b/src/Distribution/Server/Features/PackageList.hs @@ -31,6 +31,7 @@ import Distribution.PackageDescription.Configuration import Distribution.Utils.ShortText (fromShortText) import Control.Concurrent +import qualified Data.List.NonEmpty as NE import Data.Maybe (mapMaybe) import Data.Map (Map) import qualified Data.Map as Map @@ -113,7 +114,7 @@ initListFeature _env = do itemUpdate <- newHook return $ \core@CoreFeature{..} - revs@ReverseFeature{revPackageStats, reverseHook} + revs@ReverseFeature{revDirectCount, reverseHook} download votesf@VotesFeature{..} tagsf@TagsFeature{..} @@ -142,12 +143,15 @@ initListFeature _env = do runHook_ itemUpdate (Set.singleton pkgname) Nothing -> return () - registerHook reverseHook $ \pkgids -> do - let pkgs = map pkgName pkgids - forM_ pkgs $ \pkgname -> do - revCount <- revPackageStats pkgname - modifyItem pkgname (updateReverseItem revCount) - runHook_ itemUpdate $ Set.fromDistinctAscList pkgs + registerHook reverseHook $ \pkginfos -> do + let + names = Set.fromDistinctAscList $ + map (pkgName . pkgInfoId . NE.head) + pkginfos + forM_ names $ \pkgname -> do + revDirect <- revDirectCount pkgname + modifyItem pkgname (updateReverseItem revDirect) + runHook_ itemUpdate names registerHook votesUpdated $ \(pkgname, _) -> do votes <- pkgNumScore pkgname @@ -182,7 +186,7 @@ listFeature :: CoreFeature PackageName -> IO ()) listFeature CoreFeature{..} - ReverseFeature{revPackageStats} + ReverseFeature{revDirectCount} DownloadFeature{..} VotesFeature{..} TagsFeature{..} @@ -247,7 +251,7 @@ listFeature CoreFeature{..} constructItem :: PkgInfo -> IO (PackageName, PackageItem) constructItem pkg = do let pkgname = packageName pkg - revCount <- revPackageStats pkgname + intRevDirectCount <- revDirectCount pkgname users <- queryGetUserDb tags <- queryTagsForPackage pkgname downs <- recentPackageDownloads @@ -262,8 +266,8 @@ listFeature CoreFeature{..} , itemDownloads = cmFind pkgname downs , itemVotes = votes , itemLastUpload = fst (pkgOriginalUploadInfo pkg) - , itemRevDepsCount = directCount revCount - , itemHotness = votes + fromIntegral (cmFind pkgname downs) + fromIntegral (directCount revCount)*2 + , itemRevDepsCount = intRevDirectCount + , itemHotness = votes + fromIntegral (cmFind pkgname downs) + fromIntegral intRevDirectCount * 2 } ------------------------------ @@ -308,7 +312,7 @@ updateVoteItem :: Float -> PackageItem -> PackageItem updateVoteItem score item = item { itemVotes = score, - itemHotness = fromIntegral (itemRevDepsCount item)*2 + score + fromIntegral (itemDownloads item) + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + score + fromIntegral (itemDownloads item) } updateDeprecation :: Maybe [PackageName] -> PackageItem -> PackageItem @@ -317,16 +321,16 @@ updateDeprecation pkgs item = itemDeprecated = pkgs } -updateReverseItem :: ReverseCount -> PackageItem -> PackageItem -updateReverseItem revCount item = +updateReverseItem :: Int -> PackageItem -> PackageItem +updateReverseItem revDirectCount item = item { - itemRevDepsCount = directCount revCount, - itemDownloads = directCount revCount, - itemHotness = fromIntegral (itemRevDepsCount item)*2 + itemVotes item + fromIntegral (itemDownloads item) + itemRevDepsCount = revDirectCount, + itemHotness = fromIntegral revDirectCount * 2 + itemVotes item + fromIntegral (itemDownloads item) } updateDownload :: Int -> PackageItem -> PackageItem updateDownload count item = item { - itemDownloads = count + itemDownloads = count, + itemHotness = fromIntegral (itemRevDepsCount item) * 2 + itemVotes item + realToFrac count } diff --git a/src/Distribution/Server/Features/ReverseDependencies.hs b/src/Distribution/Server/Features/ReverseDependencies.hs index d2ebadb72..1a226b2c1 100644 --- a/src/Distribution/Server/Features/ReverseDependencies.hs +++ b/src/Distribution/Server/Features/ReverseDependencies.hs @@ -14,8 +14,8 @@ import Distribution.Server.Features.Core import Distribution.Server.Features.PreferredVersions import Distribution.Server.Features.PreferredVersions.State (PreferredVersions) import qualified Distribution.Server.Packages.PackageIndex as PackageIndex -import Distribution.Server.Packages.PackageIndex (PackageIndex, packageNames, allPackagesByName) -import Distribution.Server.Packages.Types (PkgInfo, pkgInfoId) +import Distribution.Server.Packages.PackageIndex (PackageIndex, packageNames, allPackagesByNameNE) +import Distribution.Server.Packages.Types (PkgInfo) import Distribution.Server.Features.ReverseDependencies.State import Distribution.Package import Distribution.Text (display) @@ -26,6 +26,7 @@ import Data.Aeson import Data.ByteString.Lazy (ByteString) import Data.Containers.ListUtils (nubOrd) import Data.List (mapAccumL, sortOn) +import qualified Data.List.NonEmpty as NE import Data.Maybe (catMaybes, fromJust) import Data.Function (fix) import qualified Data.Bimap as Bimap @@ -35,14 +36,13 @@ import qualified Data.Map as Map import Data.Set (Set) import qualified Data.Set as Set import GHC.Generics hiding (packageName) -import GHC.Stack data ReverseFeature = ReverseFeature { reverseFeatureInterface :: HackageFeature, reverseResource :: ReverseResource, - reverseHook :: Hook [PackageId] (), + reverseHook :: Hook [NE.NonEmpty PkgInfo] (), queryReverseDeps :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ([PackageName], [PackageName]), revPackageId :: forall m. (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay, @@ -50,6 +50,7 @@ data ReverseFeature = ReverseFeature { renderReverseRecent :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, renderReverseOld :: forall m. (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> m ReversePageRender, revPackageFlat :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)], + revDirectCount :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m Int, revPackageStats :: forall m. (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount, revCountForAllPackages :: forall m. (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)], revJSON :: forall m. (MonadIO m, MonadThrow m) => m ByteString, @@ -95,9 +96,9 @@ initReverseFeature _ = do Just pkginfo -> do index <- queryGetPackageIndex r <- readMemState memState - added <- addPackage (packageName pkgid) (getAllDependencies pkginfo index) r + added <- addPackage index (packageName pkgid) (getDepNames pkginfo) r writeMemState memState added - runHook_ updateReverse [pkgid] + runHook_ updateReverse [pure pkginfo] return feature @@ -128,7 +129,7 @@ instance ToJSON Edge reverseFeature :: IO (PackageIndex PkgInfo) -> IO PreferredVersions -> MemState ReverseIndex - -> Hook [PackageId] () + -> Hook [NE.NonEmpty PkgInfo] () -> ReverseFeature reverseFeature queryGetPackageIndex @@ -153,8 +154,7 @@ reverseFeature queryGetPackageIndex initReverseIndex = do index <- liftIO queryGetPackageIndex -- We build the proper index earlier, this just fires the reverse hooks - let allPackages = map pkgInfoId $ concat $ allPackagesByName index - runHook_ reverseHook allPackages + runHook_ reverseHook $ allPackagesByNameNE index reverseResource = fix $ \r -> ReverseResource @@ -187,14 +187,14 @@ reverseFeature queryGetPackageIndex let indirect = Set.difference rdepsall rdeps return (Set.toList rdeps, Set.toList indirect) - revPackageId :: (HasCallStack, MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay + revPackageId :: (MonadCatch m, MonadIO m) => PackageId -> m ReverseDisplay revPackageId pkgid = do dispInfo <- revDisplayInfo pkgIndex <- liftIO queryGetPackageIndex revs <- queryReverseIndex perVersionReverse dispInfo pkgIndex revs pkgid - revPackageName :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay + revPackageName :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseDisplay revPackageName pkgname = do dispInfo <- revDisplayInfo pkgIndex <- liftIO queryGetPackageIndex @@ -203,7 +203,7 @@ reverseFeature queryGetPackageIndex revJSON :: (MonadIO m, MonadThrow m) => m ByteString revJSON = do - ReverseIndex revdeps nodemap <- queryReverseIndex + ReverseIndex revdeps nodemap _depmap <- queryReverseIndex let assoc = takeWhile (\(a,_) -> a < Bimap.size nodemap) $ Arr.assocs . Gr.transposeG $ revdeps nodeToString node = unPackageName (nodemap Bimap.!> node) -- nodes = map (uncurry Node) $ map (\n -> (fst n, nodeToString (fst n))) assoc @@ -216,7 +216,7 @@ reverseFeature queryGetPackageIndex prefs <- liftIO queryGetPreferredVersions return $ getDisplayInfo prefs pkgIndex - renderReverseWith :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender + renderReverseWith :: (MonadIO m, MonadCatch m) => PackageName -> ReverseDisplay -> (Maybe VersionStatus -> Bool) -> m ReversePageRender renderReverseWith pkg rev filterFunc = do let rev' = map fst $ Map.toList rev directCounts <- mapM revDirectCount (pkg:rev') @@ -244,7 +244,7 @@ reverseFeature queryGetPackageIndex -- -- This could also differentiate between direct and indirect dependencies -- -- with a bit more calculation. - revPackageFlat :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)] + revPackageFlat :: (MonadIO m, MonadCatch m) => PackageName -> m [(PackageName, Int)] revPackageFlat pkgname = do memState <- readMemState reverseMemState deps <- getDependenciesFlat pkgname memState @@ -252,12 +252,12 @@ reverseFeature queryGetPackageIndex counts <- mapM (`getTotalCount` memState) depList return $ zip depList counts - revPackageStats :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m ReverseCount + revPackageStats :: (MonadIO m, MonadCatch m) => PackageName -> m ReverseCount revPackageStats pkgname = do (direct, transitive) <- getReverseCount pkgname =<< readMemState reverseMemState return $ ReverseCount direct transitive - revDirectCount :: (HasCallStack, MonadIO m, MonadCatch m) => PackageName -> m Int + revDirectCount :: (MonadIO m, MonadCatch m) => PackageName -> m Int revDirectCount pkgname = do getDirectCount pkgname =<< readMemState reverseMemState @@ -270,7 +270,7 @@ reverseFeature queryGetPackageIndex -- broken packages. -- -- The returned list is sorted ascendingly on directCount (see ReverseCount). - revCountForAllPackages :: (HasCallStack, MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)] + revCountForAllPackages :: (MonadIO m, MonadCatch m) => m [(PackageName, ReverseCount)] revCountForAllPackages = do index <- liftIO queryGetPackageIndex let pkgnames = packageNames index @@ -279,7 +279,7 @@ reverseFeature queryGetPackageIndex revForEachVersion :: (MonadThrow m, MonadIO m) => PackageName -> m (Map.Map Version (Set PackageIdentifier)) revForEachVersion pkg = do - ReverseIndex revs nodemap <- readMemState reverseMemState + ReverseIndex revs nodemap depmap <- readMemState reverseMemState index <- liftIO queryGetPackageIndex nodeid <- Bimap.lookup pkg nodemap revDepNames <- mapM (`Bimap.lookupR` nodemap) (Set.toList $ suc revs nodeid) @@ -289,5 +289,5 @@ reverseFeature queryGetPackageIndex revDepVersions = do x <- nubOrd revDepNames pkginfo <- PackageIndex.lookupPackageName index pkg - pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x) + pure (packageVersion pkginfo, dependsOnPkg index (packageId pkginfo) x depmap) pure $ Map.fromListWith Set.union revDepVersions diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index 97f423180..5a12137d5 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -1,4 +1,6 @@ -{-# LANGUAGE RankNTypes, DeriveGeneric, TypeSynonymInstances, DeriveDataTypeable, GeneralizedNewtypeDeriving, TypeFamilies, TemplateHaskell, ScopedTypeVariables #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} module Distribution.Server.Features.ReverseDependencies.State ( ReverseIndex(..) @@ -9,7 +11,7 @@ module Distribution.Server.Features.ReverseDependencies.State , constructReverseIndex , dependsOnPkg , emptyReverseIndex - , getAllDependencies + , getDepNames , getDependencies , getDependenciesFlat , getDependenciesFlatRaw @@ -26,6 +28,7 @@ module Distribution.Server.Features.ReverseDependencies.State import Prelude hiding (lookup) +import Control.Arrow ((&&&)) import Control.Monad (forM) import Control.Monad.Catch import Control.Monad.Reader (MonadIO) @@ -36,14 +39,12 @@ import Data.Containers.ListUtils (nubOrd) import Data.List (union) import Data.Map (Map) import qualified Data.Map as Map -import Data.Maybe (maybeToList, mapMaybe) -import Data.SafeCopy hiding (Version) +import Data.Maybe (catMaybes, mapMaybe, maybeToList) import qualified Data.Set as Set import Data.Set (Set, fromList, toList, delete) import Data.Typeable (Typeable) import Data.Graph (Graph, Vertex) import qualified Data.Graph as Gr -import GHC.Stack (HasCallStack, callStack, prettyCallStack) import Distribution.Package import Distribution.PackageDescription @@ -54,90 +55,104 @@ import Distribution.Server.Packages.PackageIndex (PackageIndex) import qualified Distribution.Server.Packages.PackageIndex as PackageIndex import Distribution.Version +import Data.Binary (encode) +import qualified Data.ByteString.Lazy as BS emptyReverseIndex :: ReverseIndex -emptyReverseIndex = ReverseIndex (Gr.buildG (0,0) []) Bimap.empty +emptyReverseIndex = ReverseIndex (Gr.buildG (0,0) []) Bimap.empty mempty type NodeId = Int type RevDeps = Graph -data ReverseIndex = ReverseIndex { - reverseDependencies :: !RevDeps, - packageNodeIdMap :: !(Bimap PackageName NodeId) -} deriving (Eq, Show, Typeable) +data ReverseIndex = ReverseIndex + { reverseDependencies :: !RevDeps + , packageNodeIdMap :: !(Bimap PackageName NodeId) + , deps :: Map PackageIdentifier [Dependency] + } deriving (Eq, Show, Typeable) -instance MemSize ReverseIndex where - memSize (ReverseIndex a b) = memSize2 a b +instance MemSize Dependency where + memSize = fromIntegral . BS.length . encode +instance MemSize ReverseIndex where + memSize (ReverseIndex a b c) = memSize3 a b c -constructReverseIndex :: (MonadCatch m, HasCallStack) => PackageIndex PkgInfo -> m ReverseIndex +constructReverseIndex :: MonadCatch m => PackageIndex PkgInfo -> m ReverseIndex constructReverseIndex index = do let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] - revs <- constructRevDeps index nodePkgMap + (revs, deps) <- constructRevDeps index nodePkgMap pure $ - ReverseIndex { - reverseDependencies = revs, - packageNodeIdMap = nodePkgMap - } + ReverseIndex + { reverseDependencies = revs + , packageNodeIdMap = nodePkgMap + , deps = deps + } -addPackage :: (HasCallStack, MonadCatch m, MonadIO m) => HasCallStack => PackageName -> [PackageName] +addPackage :: (MonadCatch m, MonadIO m) => PackageIndex PkgInfo -> PackageName -> [PackageName] -> ReverseIndex -> m ReverseIndex -addPackage pkgid deps ri@(ReverseIndex revs nodemap) = do +addPackage index pkgname deps ri@(ReverseIndex revs nodemap pkgIdToDeps) = do let - npm = Bimap.tryInsert pkgid (Bimap.size nodemap) nodemap + npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap new :: [(Int, [Int])] <- forM deps $ \d -> - (,) <$> looky d npm <*> fmap (:[]) (looky pkgid npm) - --pure (npm ! d, [npm ! pkgid]) + (,) <$> lookup d npm <*> fmap (:[]) (lookup pkgname npm) let rd = insEdges (Bimap.size npm) new revs - pure $ - ri { - reverseDependencies = rd, - packageNodeIdMap = npm - } + pkginfos = PackageIndex.lookupPackageName index pkgname + newPackageDepMap = Map.fromList $ map (packageId &&& getDeps) pkginfos + pure + ri + { reverseDependencies = rd + , packageNodeIdMap = npm + , deps = Map.union newPackageDepMap pkgIdToDeps + } -constructRevDeps :: forall m. (MonadCatch m, HasCallStack) => PackageIndex PkgInfo -> Bimap PackageName NodeId -> m RevDeps +constructRevDeps :: forall m. MonadCatch m => PackageIndex PkgInfo -> Bimap PackageName NodeId -> m (RevDeps, Map PackageIdentifier [Dependency]) constructRevDeps index nodemap = do let allPackages :: [PkgInfo] - allPackages = concat {- concatMap (take 5) -} $ PackageIndex.allPackagesByName index - g :: PkgInfo -> m [(NodeId, NodeId)] - g pkg = mapM (f pkg) (getAllDependencies pkg index) - f :: PkgInfo -> PackageName -> m (NodeId, NodeId) - f pkg dep = (,) <$> (lookup dep nodemap) <*> (lookup (packageName pkg) nodemap :: m NodeId) - edges <- traverse g allPackages - - pure $ Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) - -getAllDependencies :: forall pkg. Package pkg => PkgInfo -> PackageIndex pkg -> [PackageName] -getAllDependencies pkg index = - map packageName $ toDepsList (maybeToList $ condLibrary desc) ++ toDepsList (map snd $ condExecutables desc) - where - desc = pkgDesc pkg - toDepsList :: [CondTree v [Dependency] a] -> [pkg] - toDepsList l = concatMap (PackageIndex.lookupDependency index) $ concatMap harvestDependencies l - -- | Collect all dependencies from all branches of a condition tree. - harvestDependencies :: CondTree v [Dependency] a -> [Dependency] - harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps - where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + allPackages = concat $ PackageIndex.allPackagesByName index + nodeIdsOfDependencies :: PkgInfo -> m [(NodeId, NodeId)] + nodeIdsOfDependencies pkg = catMaybes <$> mapM findNodesIfPresent (getDepNames pkg) + where + findNodesIfPresent :: PackageName -> m (Maybe (NodeId, NodeId)) + findNodesIfPresent dep = do + eitherErrOrFound :: Either SomeException (NodeId, NodeId) <- + try $ (,) <$> lookup dep nodemap <*> lookup (packageName pkg) nodemap + pure $ either (const Nothing) Just eitherErrOrFound + -- This will mix dependencies of different versions of the same package, but that is intended. + edges <- traverse nodeIdsOfDependencies allPackages + let deps = Map.fromList $ map (packageId &&& getDeps) allPackages + + pure (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) + , deps + ) + +getDeps :: PkgInfo -> [Dependency] +getDeps pkg = + concatMap harvestDependencies (maybeToList $ condLibrary $ pkgDesc pkg) + +getDepNames :: PkgInfo -> [PackageName] +getDepNames pkg = + map depPkgName $ getDeps pkg -- | Returns [containers 0.5.0.0 to 0.6.0.1] for needle=ghc-prim-0.6.0.0 and packageHaystack=containers -- | because these are the versions that could accept that version of ghc-prim as a dep -- | Note that this doesn't include executables! Only the library. -dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Set PackageIdentifier -dependsOnPkg index needle packageHaystack = +dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Map PackageIdentifier [Dependency] -> Set PackageIdentifier +dependsOnPkg index needle packageHaystack deps = fromList $ mapMaybe descPermits (PackageIndex.lookupPackageName index packageHaystack) where descPermits :: PkgInfo -> Maybe PackageIdentifier descPermits pkginfo - | any toDepsList . concatMap harvestDependencies . condLibrary $ pkgDesc pkginfo = Just (packageId pkginfo) + | Just found <- Map.lookup (packageId pkginfo) deps + , any toDepsList found = Just (packageId pkginfo) | otherwise = Nothing toDepsList (Dependency name versionRange _) = packageVersion needle `withinRange` versionRange && packageName needle == name - -- | Collect all dependencies from all branches of a condition tree. - harvestDependencies :: CondTree v [Dependency] a -> [Dependency] - harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps - where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree + +-- | Collect all dependencies from all branches of a condition tree. +harvestDependencies :: CondTree v [Dependency] a -> [Dependency] +harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps + where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree ---------------------------------ReverseDisplay @@ -155,28 +170,20 @@ type ReverseDisplay = Map PackageName (Version, Maybe VersionStatus) type VersionIndex = (PackageName -> (PreferredInfo, [Version])) --- | Look up 'key', but in case of failure, fails with a more informative error message than what Bimap provides -looky :: (HasCallStack, MonadCatch m, Ord a, Ord b, Show a) => a -> Bimap a b -> m b -looky key bimap = do - res <- try (lookup key bimap) - case res of - Left (_ :: SomeException) -> throwM $ userError $ "couldn't find key " ++ show key ++ " " ++ prettyCallStack callStack - Right a -> pure a - -perPackageReverse :: (HasCallStack, MonadCatch m) => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> m (Map PackageName (Version, Maybe VersionStatus)) +perPackageReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageName -> m (Map PackageName (Version, Maybe VersionStatus)) perPackageReverse indexFunc index revdeps pkg = do let pkgids = (packageVersion. packageId) <$> PackageIndex.lookupPackageName index pkg let best :: PackageId best = PackageIdentifier pkg (maximum pkgids) perVersionReverse indexFunc index revdeps best -perVersionReverse :: (HasCallStack, MonadCatch m) => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) -perVersionReverse indexFunc index (ReverseIndex revs nodemap) pkg = do +perVersionReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) +perVersionReverse indexFunc index (ReverseIndex revs nodemap deps) pkg = do found <- lookup (packageName pkg) nodemap -- this will be too much, since we are throwing away the specific version revDepNames :: Set PackageName <- fromList <$> mapM (`lookupR` nodemap) (toList $ suc revs found) let packagemap :: Map PackageName (Set Version) - packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x)) (toList revDepNames) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x deps)) (toList revDepNames) pure $ constructReverseDisplay indexFunc packagemap constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) @@ -200,35 +207,24 @@ insEdges nodesize edges revdeps = Arr.accumArray union [] (0, nodesize) (edges + -------------------------------------- -instance (SafeCopy a, SafeCopy b, Ord a, Ord b) => SafeCopy (Bimap a b) where - getCopy = contain $ fmap Bimap.fromList safeGet - putCopy = contain . safePut . Bimap.toList - --- instance (SafeCopy a, SafeCopy b) => SafeCopy (Graph) where --- putCopy = contain . safePut . Gr.edges --- getCopy = contain $ fmap (Gr.buildG (0,maxNodes)) safeGet - -$(deriveSafeCopy 0 'base ''ReverseIndex) -$(deriveSafeCopy 0 'base ''ReverseCount) - -getDependencies :: (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set PackageName) +getDependencies :: MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) getDependencies pkg revs = names revs =<< getDependenciesRaw pkg revs -getDependenciesRaw :: (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set NodeId) -getDependenciesRaw pkg (ReverseIndex revdeps nodemap) = do - enodeid <- try (looky pkg nodemap) +getDependenciesRaw :: MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesRaw pkg (ReverseIndex revdeps nodemap _) = do + enodeid <- try (lookup pkg nodemap) onRight enodeid $ \nodeid -> nodeid `delete` suc revdeps nodeid -- | The flat/total/transitive/indirect reverse dependencies are all the packages that depend on something that depends on the given 'pkg' -getDependenciesFlat :: forall m. (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set PackageName) +getDependenciesFlat :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set PackageName) getDependenciesFlat pkg revs = names revs =<< getDependenciesFlatRaw pkg revs -getDependenciesFlatRaw :: forall m. (MonadCatch m, HasCallStack) => PackageName -> ReverseIndex -> m (Set NodeId) -getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap) = do - enodeid <- try (looky pkg nodemap) +getDependenciesFlatRaw :: forall m. MonadCatch m => PackageName -> ReverseIndex -> m (Set NodeId) +getDependenciesFlatRaw pkg (ReverseIndex revdeps nodemap _) = do + enodeid <- try (lookup pkg nodemap) onRight enodeid $ \nodeid -> nodeid `delete` fromList (Gr.reachable revdeps nodeid) @@ -239,7 +235,7 @@ getDirectCount pkg revs = do -- | Given a set of NodeIds, look up the package names for all of them names :: MonadThrow m => ReverseIndex -> Set NodeId -> m (Set PackageName) -names (ReverseIndex _ nodemap) ids = do +names (ReverseIndex _ nodemap _) ids = do fromList <$> mapM (`lookupR` nodemap) (toList ids) onRight :: Monad m => Either SomeException t -> (t -> Set NodeId) -> m (Set NodeId) @@ -255,7 +251,7 @@ getTotalCount :: MonadCatch m => PackageName -> ReverseIndex -> m Int getTotalCount pkg revs = do length <$> getDependenciesFlatRaw pkg revs -getReverseCount :: (HasCallStack, MonadCatch m) => PackageName -> ReverseIndex -> m (Int, Int) +getReverseCount :: MonadCatch m => PackageName -> ReverseIndex -> m (Int, Int) getReverseCount pkg revs = do direct <- getDirectCount pkg revs total <- getTotalCount pkg revs diff --git a/src/Distribution/Server/Packages/PackageIndex.hs b/src/Distribution/Server/Packages/PackageIndex.hs index 4b862d649..228e5b768 100644 --- a/src/Distribution/Server/Packages/PackageIndex.hs +++ b/src/Distribution/Server/Packages/PackageIndex.hs @@ -44,7 +44,8 @@ module Distribution.Server.Packages.PackageIndex ( -- ** Bulk queries allPackageNames, allPackages, - allPackagesByName + allPackagesByName, + allPackagesByNameNE ) where import Distribution.Server.Prelude hiding (lookup) @@ -58,6 +59,7 @@ import qualified Data.Map.Strict as Map import Data.Map.Strict (Map) import qualified Data.Foldable as Foldable import Data.List (groupBy, find, isInfixOf) +import qualified Data.List.NonEmpty as NE import Data.SafeCopy import Distribution.Types.PackageName @@ -255,6 +257,9 @@ allPackages (PackageIndex m) = concat (Map.elems m) -- -- They are grouped by package name, case-sensitively. -- +allPackagesByNameNE :: Package pkg => PackageIndex pkg -> [NE.NonEmpty pkg] +allPackagesByNameNE (PackageIndex m) = map NE.fromList $ Map.elems m + allPackagesByName :: Package pkg => PackageIndex pkg -> [[pkg]] allPackagesByName (PackageIndex m) = Map.elems m diff --git a/tests/RevDepCommon.hs b/tests/RevDepCommon.hs index 93c27d633..5c8514caf 100644 --- a/tests/RevDepCommon.hs +++ b/tests/RevDepCommon.hs @@ -4,7 +4,6 @@ module RevDepCommon where import GHC.Generics (Generic) import Distribution.Server.Users.Types (UserId(..)) -import Data.List (intercalate) import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) import Distribution.Server.Packages.Types (PkgInfo(..)) import qualified Data.Vector as Vector @@ -16,17 +15,17 @@ import Distribution.Version (mkVersion, versionNumbers) data Package b = Package - { name :: b - , version :: Int - , deps :: [ b ] + { pName :: b + , pVersion :: Int + , pDeps :: [ b ] } deriving (Ord, Show, Eq) packToPkgInfo :: Show b => Package b -> PkgInfo -packToPkgInfo Package {name, version, deps} = - mkPackage (mkPackageName $ show name) [version] (depsToBS deps) +packToPkgInfo Package {pName, pVersion, pDeps} = + mkPackage (mkPackageName $ show pName) [pVersion] (depsToBS pDeps) -mkPackage :: PackageName -> [Int] -> BSL.ByteString -> PkgInfo +mkPackage :: PackageName -> [Int] -> [BSL.ByteString] -> PkgInfo mkPackage name intVersion depends = let version = mkVersion intVersion @@ -38,19 +37,16 @@ mkPackage name intVersion depends = \name: " <> BSL.fromStrict (Char8.pack $ unPackageName name) <> "\n\ \version: " <> dotVersion <> "\n" cabalFile :: CabalFileText - cabalFile = CabalFileText $ cabalFilePrefix <> if depends /= "" then "library\n build-depends: " <> depends else "" + cabalFile = CabalFileText $ cabalFilePrefix <> if depends /= [] then "library\n build-depends: " <> BSL.intercalate "," depends else "" in PkgInfo (PackageIdentifier name version) (Vector.fromList [(cabalFile, (UTCTime (fromGregorian 2020 1 1) 0, UserId 1))]) mempty -depsToBS :: Show b => [ b ] -> BSL.ByteString -depsToBS deps = - let - depsBS :: String - depsBS = intercalate "," $ map show deps - in BSL.fromStrict $ Char8.pack $ depsBS +depsToBS :: Show b => [ b ] -> [BSL.ByteString] +depsToBS = + map (BSL.fromStrict . Char8.pack . show) newtype TestPackage = TestPackage Word diff --git a/tests/ReverseDependenciesTest.hs b/tests/ReverseDependenciesTest.hs index 437e88e16..6c2bbf94d 100644 --- a/tests/ReverseDependenciesTest.hs +++ b/tests/ReverseDependenciesTest.hs @@ -32,12 +32,12 @@ import RevDepCommon (Package(..), TestPackage(..), mkPackage, packToPkgInfo) mtlBeelineLens :: [PkgInfo] mtlBeelineLens = - [ mkPackage "base" [4,15] "" - , mkPackage "mtl" [2,3] "base" + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] -- Note that this example is a bit unrealistic -- since these two do not depend on base... - , mkPackage "beeline" [0] "mtl" - , mkPackage "lens" [0] "mtl" + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "lens" [0] ["mtl"] ] mkRevFeat :: [PkgInfo] -> IO ReverseFeature @@ -51,7 +51,7 @@ mkRevFeat pkgs = do , migratedEphemeralPrefs = False } updateReverse <- newHook - Right constructed <- pure $ constructReverseIndex idx + constructed <- constructReverseIndex idx memState <- newMemStateWHNF constructed pure $ reverseFeature @@ -64,8 +64,9 @@ allTests :: TestTree allTests = testGroup "ReverseDependenciesTest" [ testCase "with set [beeline->mtl] and querying for mtl, we get beeline" $ do let pkgs = - [ mkPackage "mtl" [2,3] "base" - , mkPackage "beeline" [0] "mtl" + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] ] ReverseFeature{revPackageName} <- mkRevFeat pkgs res <- revPackageName "mtl" @@ -73,22 +74,24 @@ allTests = testGroup "ReverseDependenciesTest" assertEqual "reverse dependencies must be [beeline]" ref res , testCase "revPackageName selects only the version with an actual dependency, even if it is not the newest" $ do let pkgs = - [ mkPackage "mtl" [2,3] "base" - , mkPackage "mtl-tf" [9000] "base" - , mkPackage "beeline" [0] "mtl" - , mkPackage "beeline" [1] "mtl-tf" + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] ] ReverseFeature{revPackageName} <- mkRevFeat pkgs res <- revPackageName "mtl" let ref = Map.fromList [("beeline", (mkVersion [0], Nothing))] assertEqual "reverse dependencies must be [beeline v0]" ref res , testCase "revPackageId does select old version when queried with old reverse dependency" $ do - let mtl = mkPackage "mtl" [2,3] "base" + let mtl = mkPackage "mtl" [2,3] ["base"] pkgs = - [ mtl - , mkPackage "mtl-tf" [9000] "base" - , mkPackage "beeline" [0] "mtl" - , mkPackage "beeline" [1] "mtl-tf" + [ mkPackage "base" [4,15] [] + , mtl + , mkPackage "mtl-tf" [9000] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "beeline" [1] ["mtl-tf"] ] ReverseFeature{revPackageId} <- mkRevFeat pkgs res <- revPackageId (packageId mtl) @@ -97,9 +100,10 @@ allTests = testGroup "ReverseDependenciesTest" assertEqual "reverse dependencies must be [beeline v0]" ref res , testCase "revPackageName can find multiple packages" $ do let pkgs = - [ mkPackage "mtl" [2,3] "base" - , mkPackage "beeline" [0] "mtl" - , mkPackage "mario" [0] "mtl" + [ mkPackage "base" [4,15] [] + , mkPackage "mtl" [2,3] ["base"] + , mkPackage "beeline" [0] ["mtl"] + , mkPackage "mario" [0] ["mtl"] ] ReverseFeature{revPackageName} <- mkRevFeat pkgs res <- revPackageName "mtl" @@ -147,8 +151,8 @@ prop_constructRevDeps :: Property prop_constructRevDeps = property $ do packs <- genPacks let idx = PackageIndex.fromList $ map packToPkgInfo packs - ReverseIndex foldedDeps foldedMap <- foldM (packageFolder @_ @TestPackage) emptyReverseIndex packs - Right (ReverseIndex constructedDeps constructedMap) <- pure $ constructReverseIndex idx + ReverseIndex foldedRevDeps foldedMap foldedDeps <- foldM (packageFolder @_ @TestPackage idx) emptyReverseIndex packs + Right (ReverseIndex constructedRevDeps constructedMap constructedDeps) <- pure $ constructReverseIndex idx for_ (PackageIndex.allPackageNames idx) $ \name -> do foundFolded :: Int <- Bimap.lookup name foldedMap foundConstructed :: Int <- Bimap.lookup name constructedMap @@ -157,10 +161,12 @@ prop_constructRevDeps = property $ do -- foundFolded === foundConstructed -- but they should have the same deps - foldedPackNames <- mapM (`Bimap.lookupR` foldedMap) (foldedDeps Arr.! foundFolded) - constructedPackNames <- mapM (`Bimap.lookupR` constructedMap) (constructedDeps Arr.! foundConstructed) + foldedPackNames <- mapM (`Bimap.lookupR` foldedMap) (foldedRevDeps Arr.! foundFolded) + constructedPackNames <- mapM (`Bimap.lookupR` constructedMap) (constructedRevDeps Arr.! foundConstructed) Set.fromList foldedPackNames === Set.fromList constructedPackNames + foldedDeps === constructedDeps + prop_statsEqualsDeps :: Property prop_statsEqualsDeps = property $ do packs <- genPacks @@ -175,9 +181,9 @@ prop_statsEqualsDeps = property $ do length directSet === length directNames length totalSet === length totalNames -packageFolder :: (MonadCatch m, MonadIO m, MonadTest m, Show b) => ReverseIndex -> Package b -> m ReverseIndex -packageFolder index pkg@(Package name _version deps) = - catch (liftIO $ addPackage (mkPackageName $ show name) (map (mkPackageName . show) deps) index) +packageFolder :: (MonadCatch m, MonadIO m, MonadTest m, Show b) => PackageIndex PkgInfo -> ReverseIndex -> Package b -> m ReverseIndex +packageFolder index revindex pkg@(Package name _version deps) = + catch (liftIO $ addPackage index (mkPackageName $ show name) (map (mkPackageName . show) deps) revindex) $ \(e :: SomeException) -> do footnoteShow pkg footnoteShow index @@ -187,9 +193,9 @@ packageFolder index pkg@(Package name _version deps) = genPackage :: forall m b. (MonadGen m, Enum b, Bounded b, Ord b) => b -> [Package b] -> m (Package b) genPackage newName available = do - version <- Gen.int (Range.linear 0 10) + pVersion <- Gen.int (Range.linear 0 10) depPacks :: [Package b] <- Gen.subsequence available - pure $ Package {name = newName, version, deps = map name depPacks } + pure $ Package {pName = newName, pVersion, pDeps = map pName depPacks } packsUntil :: forall m b. (Ord b, Bounded b, MonadGen m, Enum b) => Bool -> Int -> [Package b] -> m [Package b] packsUntil allowMultipleVersions limit generated | length generated < limit = do @@ -199,11 +205,11 @@ packsUntil allowMultipleVersions limit generated | length generated < limit = do then genPackage (toEnum $ length generated) generated else do - Package { name = prevName } <- Gen.element generated - let (prevNamePacks, nonPrevName) = partition ((== prevName) . name) generated + Package { pName = prevName } <- Gen.element generated + let (prevNamePacks, nonPrevName) = partition ((== prevName) . pName) generated depPacks :: [Package b] <- Gen.subsequence nonPrevName - let newVersion = 1 + maximum (map version prevNamePacks) - pure $ Package {name = prevName, version = newVersion, deps = map name depPacks} + let newVersion = 1 + maximum (map pVersion prevNamePacks) + pure $ Package {pName = prevName, pVersion = newVersion, pDeps = map pName depPacks} let added = generated ++ [toInsert] packsUntil allowMultipleVersions limit added packsUntil _ _ generated = pure generated From d26133b38c32cbf6dfdfb445cbb67e3eef8a306c Mon Sep 17 00:00:00 2001 From: Janus Troelsen Date: Tue, 19 Jul 2022 21:36:21 -0500 Subject: [PATCH 4/6] Remove limit of 10 versions, fix warnings, format code --- .../Features/ReverseDependencies/State.hs | 22 +++++++++---------- src/Distribution/Server/Pages/Reverse.hs | 16 +++++--------- tests/RevDepCommon.hs | 22 +++++++++++-------- 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/src/Distribution/Server/Features/ReverseDependencies/State.hs b/src/Distribution/Server/Features/ReverseDependencies/State.hs index 5a12137d5..e4f9a9632 100644 --- a/src/Distribution/Server/Features/ReverseDependencies/State.hs +++ b/src/Distribution/Server/Features/ReverseDependencies/State.hs @@ -79,21 +79,21 @@ instance MemSize ReverseIndex where constructReverseIndex :: MonadCatch m => PackageIndex PkgInfo -> m ReverseIndex constructReverseIndex index = do let nodePkgMap = foldr (uncurry Bimap.insert) Bimap.empty $ zip (PackageIndex.allPackageNames index) [0..] - (revs, deps) <- constructRevDeps index nodePkgMap + (revs, dependencies) <- constructRevDeps index nodePkgMap pure $ ReverseIndex { reverseDependencies = revs , packageNodeIdMap = nodePkgMap - , deps = deps + , deps = dependencies } addPackage :: (MonadCatch m, MonadIO m) => PackageIndex PkgInfo -> PackageName -> [PackageName] -> ReverseIndex -> m ReverseIndex -addPackage index pkgname deps ri@(ReverseIndex revs nodemap pkgIdToDeps) = do +addPackage index pkgname dependencies ri@(ReverseIndex revs nodemap pkgIdToDeps) = do let npm = Bimap.tryInsert pkgname (Bimap.size nodemap) nodemap new :: [(Int, [Int])] <- - forM deps $ \d -> + forM dependencies $ \d -> (,) <$> lookup d npm <*> fmap (:[]) (lookup pkgname npm) let rd = insEdges (Bimap.size npm) new revs pkginfos = PackageIndex.lookupPackageName index pkgname @@ -119,10 +119,10 @@ constructRevDeps index nodemap = do pure $ either (const Nothing) Just eitherErrOrFound -- This will mix dependencies of different versions of the same package, but that is intended. edges <- traverse nodeIdsOfDependencies allPackages - let deps = Map.fromList $ map (packageId &&& getDeps) allPackages + let dependencies = Map.fromList $ map (packageId &&& getDeps) allPackages pure (Gr.buildG (0, Bimap.size nodemap) (nubOrd $ concat edges) - , deps + , dependencies ) getDeps :: PkgInfo -> [Dependency] @@ -137,12 +137,12 @@ getDepNames pkg = -- | because these are the versions that could accept that version of ghc-prim as a dep -- | Note that this doesn't include executables! Only the library. dependsOnPkg :: PackageIndex PkgInfo -> PackageId -> PackageName -> Map PackageIdentifier [Dependency] -> Set PackageIdentifier -dependsOnPkg index needle packageHaystack deps = +dependsOnPkg index needle packageHaystack dependencies = fromList $ mapMaybe descPermits (PackageIndex.lookupPackageName index packageHaystack) where descPermits :: PkgInfo -> Maybe PackageIdentifier descPermits pkginfo - | Just found <- Map.lookup (packageId pkginfo) deps + | Just found <- Map.lookup (packageId pkginfo) dependencies , any toDepsList found = Just (packageId pkginfo) | otherwise = Nothing toDepsList (Dependency name versionRange _) = @@ -151,7 +151,7 @@ dependsOnPkg index needle packageHaystack deps = -- | Collect all dependencies from all branches of a condition tree. harvestDependencies :: CondTree v [Dependency] a -> [Dependency] -harvestDependencies (CondNode _ deps comps) = deps ++ concatMap forComponent comps +harvestDependencies (CondNode _ dependencies comps) = dependencies ++ concatMap forComponent comps where forComponent (CondBranch _ iftree elsetree) = harvestDependencies iftree ++ maybe [] harvestDependencies elsetree ---------------------------------ReverseDisplay @@ -178,12 +178,12 @@ perPackageReverse indexFunc index revdeps pkg = do perVersionReverse indexFunc index revdeps best perVersionReverse :: MonadCatch m => (PackageName -> (PreferredInfo, [Version])) -> PackageIndex PkgInfo -> ReverseIndex -> PackageId -> m (Map PackageName (Version, Maybe VersionStatus)) -perVersionReverse indexFunc index (ReverseIndex revs nodemap deps) pkg = do +perVersionReverse indexFunc index (ReverseIndex revs nodemap dependencies) pkg = do found <- lookup (packageName pkg) nodemap -- this will be too much, since we are throwing away the specific version revDepNames :: Set PackageName <- fromList <$> mapM (`lookupR` nodemap) (toList $ suc revs found) let packagemap :: Map PackageName (Set Version) - packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x deps)) (toList revDepNames) + packagemap = Map.fromList $ map (\x -> (x, Set.map packageVersion $ dependsOnPkg index pkg x dependencies)) (toList revDepNames) pure $ constructReverseDisplay indexFunc packagemap constructReverseDisplay :: (PackageName -> (PreferredInfo, [Version])) -> Map PackageName (Set Version) -> Map PackageName (Version, Maybe VersionStatus) diff --git a/src/Distribution/Server/Pages/Reverse.hs b/src/Distribution/Server/Pages/Reverse.hs index cffff8181..aa8a288aa 100644 --- a/src/Distribution/Server/Pages/Reverse.hs +++ b/src/Distribution/Server/Pages/Reverse.hs @@ -1,4 +1,4 @@ -{-# LANGUAGE NamedFieldPuns, RecordWildCards, NamedFieldPuns, BlockArguments #-} +{-# LANGUAGE NamedFieldPuns, RecordWildCards, BlockArguments #-} module Distribution.Server.Pages.Reverse ( ReverseHtmlUtil(..) , reverseHtmlUtil @@ -58,8 +58,8 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} [ tr ! [theclass (if odd n then "odd" else "even")] << [ td << anchor ! [href $ packageLink $ PackageIdentifier (packageName pkg) $ nullVersion ] << display (packageName pkg) , td << anchor ! (renderStatus status ++ [href $ packageLink pkg]) << display (packageVersion pkg) - , td << [ toHtml $ (show count) ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" $ packageName pkg] << "view", toHtml ")" ] ] - | (ReverseRender pkg status count, n) <- zip renders [(1::Int)..] ] + , td << [ toHtml $ (show count'') ++ " (", anchor ! [href $ reverseFlatUri reverseResource "" $ packageName pkg] << "view", toHtml ")" ] ] + | (ReverseRender pkg status count'', n) <- zip renders [(1::Int)..] ] renderStatus (Just DeprecatedVersion) = [theclass "deprecated"] renderStatus (Just UnpreferredVersion) = [theclass "unpreferred"] @@ -92,18 +92,12 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} -- /package/:package/reverse/verbose reverseVerboseRender :: PackageName -> [Version] -> (PackageId -> String) -> ReverseCount -> (Map.Map Version (Set PackageIdentifier)) -> [Html] reverseVerboseRender pkgname allVersions packageLink revCount versions = - h2 << (display pkgname ++ ": reverse dependency statistics"): + h2 << (display pkgname ++ ": reverse dependencies per version"): [ renderCount revCount , versionTable - , if length allVersions > limitVersions - -- Why the oldest? Such that the package can be cached indefinitely without having to get invalidated. - then thediv << [toHtml "Only showing the oldest ", toHtml (display limitVersions), toHtml " versions."] - else mempty ] where - limitVersions = 10 - versionTable = thediv << (table ! [theclass "fancy"]) << versionTableRows versionTableRows = (tr << [ th << "Version", th << "Reverse dependencies" ]) : @@ -111,7 +105,7 @@ reverseHtmlUtil ReverseFeature{reverseResource} = ReverseHtmlUtil{..} [ td << anchor ! [href $ packageLink pkgid ] << display version , td << [ row ] ] - | (version, n) <- take limitVersions $ zip allVersions [(1::Int)..] + | (version, n) <- zip allVersions [(1::Int)..] , let pkgid = PackageIdentifier pkgname version mkListOfLinks :: Set PackageIdentifier -> [Html] diff --git a/tests/RevDepCommon.hs b/tests/RevDepCommon.hs index 5c8514caf..a7f79ff9c 100644 --- a/tests/RevDepCommon.hs +++ b/tests/RevDepCommon.hs @@ -1,17 +1,21 @@ -{-# LANGUAGE OverloadedStrings, NamedFieldPuns #-} -{-# LANGUAGE GeneralizedNewtypeDeriving, DerivingStrategies, DeriveGeneric #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} module RevDepCommon where -import GHC.Generics (Generic) -import Distribution.Server.Users.Types (UserId(..)) -import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) -import Distribution.Server.Packages.Types (PkgInfo(..)) -import qualified Data.Vector as Vector -import Distribution.Server.Packages.Types (CabalFileText(..)) import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Char8 as Char8 +import qualified Data.Vector as Vector import Data.Time (UTCTime(..), fromGregorian) -import Distribution.Version (mkVersion, versionNumbers) +import GHC.Generics (Generic) + +import Distribution.Server.Users.Types (UserId(..)) +import Distribution.Package (PackageIdentifier(..), PackageName, mkPackageName, unPackageName) +import Distribution.Server.Packages.Types (PkgInfo(..)) +import Distribution.Server.Packages.Types (CabalFileText(..)) +import Distribution.Version (mkVersion, versionNumbers) data Package b = Package From 6f15b28e6d2c9e5a850f44d644d2b3bba343af9c Mon Sep 17 00:00:00 2001 From: Gershom Bazerman Date: Sun, 1 Jan 2023 12:00:38 -0500 Subject: [PATCH 5/6] fixup revdeps --- src/Distribution/Server/Features/PackageList.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Distribution/Server/Features/PackageList.hs b/src/Distribution/Server/Features/PackageList.hs index f63dffb17..51cbf5fe0 100644 --- a/src/Distribution/Server/Features/PackageList.hs +++ b/src/Distribution/Server/Features/PackageList.hs @@ -88,18 +88,17 @@ data PackageItem = PackageItem { -- Last upload date itemLastUpload :: !UTCTime, -- Hotness = recent downloads + stars + 2 * no rev deps - itemHotness :: !Float, + itemHotness :: !Float, -- Last version itemLastVersion :: !String } instance MemSize PackageItem where - memSize (PackageItem a b c d e f g h i j k l m n o) = memSize11 a b c d e f g h i j (k, l, m, n, o) + memSize (PackageItem a b c d e f g h i j k l _m n o) = memSize11 a b c d e f g h i j (k, l, n, o) emptyPackageItem :: PackageName -> PackageItem emptyPackageItem pkg = PackageItem pkg Set.empty Nothing "" [] - 0 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) 0 0 0 0 False 0 0 0 (UTCTime (toEnum 0) 0) 0 "" @@ -255,7 +254,7 @@ listFeature CoreFeature{..} constructItem :: PkgInfo -> IO (PackageName, PackageItem) constructItem pkg = do let pkgname = packageName pkg - desc = pkgDesc pkg + desc = pkgDesc pkg intRevDirectCount <- revDirectCount pkgname users <- queryGetUserDb tags <- queryTagsForPackage pkgname From b7ba3e01fffb5bb558997dce639df19bf5480f12 Mon Sep 17 00:00:00 2001 From: Gershom Bazerman Date: Sun, 1 Jan 2023 13:33:01 -0500 Subject: [PATCH 6/6] fmt --- datafiles/templates/Html/package-page.html.st | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafiles/templates/Html/package-page.html.st b/datafiles/templates/Html/package-page.html.st index f5e1aff51..a85f11ab8 100644 --- a/datafiles/templates/Html/package-page.html.st +++ b/datafiles/templates/Html/package-page.html.st @@ -227,7 +227,7 @@ $if(hasrdeps)$ - ReverseDependencies + Reverse Dependencies $rdeps$