List.hs 22.4 KB
Newer Older
1 2
-----------------------------------------------------------------------------
-- |
3
-- Module      :  Distribution.Client.List
4
-- Copyright   :  (c) David Himmelstrup 2005
5
--                    Duncan Coutts 2008-2011
6 7
-- License     :  BSD-like
--
8
-- Maintainer  :  cabal-devel@haskell.org
9
--
10
-- Search for and print information about packages
11
-----------------------------------------------------------------------------
12
module Distribution.Client.List (
13
  list, info
14
  ) where
15

16
import Distribution.Package
17
         ( PackageName(..), Package(..), packageName, packageVersion
Andres Löh's avatar
Andres Löh committed
18
         , Dependency(..), simplifyDependency )
19
import Distribution.ModuleName (ModuleName)
20
import Distribution.License (License)
21
import qualified Distribution.InstalledPackageInfo as Installed
22
import qualified Distribution.PackageDescription   as Source
23 24 25 26
import Distribution.PackageDescription
         ( Flag(..), FlagName(..) )
import Distribution.PackageDescription.Configuration
         ( flattenPackageDescription )
27

28 29
import Distribution.Simple.Compiler
        ( Compiler, PackageDBStack )
30
import Distribution.Simple.Program (ProgramConfiguration)
31 32
import Distribution.Simple.Utils
        ( equating, comparing, die, notice )
33
import Distribution.Simple.Setup (fromFlag)
34
import qualified Distribution.Simple.PackageIndex as InstalledPackageIndex
35
import qualified Distribution.Client.PackageIndex as PackageIndex
36 37 38
import Distribution.Version
         ( Version(..), VersionRange, withinRange, anyVersion
         , intersectVersionRanges, simplifyVersionRange )
39 40 41 42 43
import Distribution.Verbosity (Verbosity)
import Distribution.Text
         ( Text(disp), display )

import Distribution.Client.Types
Andres Löh's avatar
Andres Löh committed
44
         ( SourcePackage(..), Repo, SourcePackageDb(..) )
45
import Distribution.Client.Dependency.Types
46
         ( PackageConstraint(..), ExtDependency(..) )
47 48
import Distribution.Client.Targets
         ( UserTarget, resolveUserTargets, PackageSpecifier(..) )
49
import Distribution.Client.Setup
50
         ( GlobalFlags(..), ListFlags(..), InfoFlags(..) )
51 52 53
import Distribution.Client.Utils
         ( mergeBy, MergeResult(..) )
import Distribution.Client.IndexUtils as IndexUtils
54
         ( getSourcePackages, getInstalledPackages )
Duncan Coutts's avatar
Duncan Coutts committed
55
import Distribution.Client.FetchUtils
56 57 58
         ( isFetched )

import Data.List
59
         ( sortBy, groupBy, sort, nub, intersperse, maximumBy, partition )
60
import Data.Maybe
61 62 63
         ( listToMaybe, fromJust, fromMaybe, isJust )
import qualified Data.Map as Map
import Data.Tree as Tree
64 65 66 67
import Control.Monad
         ( MonadPlus(mplus), join )
import Control.Exception
         ( assert )
dterei's avatar
dterei committed
68
import Text.PrettyPrint as Disp
69 70
import System.Directory
         ( doesDirectoryExist )
71

72

73
-- |Show information about packages
74
list :: Verbosity
75
     -> PackageDBStack
76 77 78 79 80 81
     -> [Repo]
     -> Compiler
     -> ProgramConfiguration
     -> ListFlags
     -> [String]
     -> IO ()
82
list verbosity packageDBs repos comp conf listFlags pats = do
83

84 85 86
    installedPkgIndex <- getInstalledPackages verbosity comp packageDBs conf
    sourcePkgDb       <- getSourcePackages    verbosity repos
    let sourcePkgIndex = packageIndex sourcePkgDb
87
        prefs name = fromMaybe anyVersion
88
                       (Map.lookup name (packagePreferences sourcePkgDb))
89

90
        pkgsInfo :: [(PackageName, [Installed.InstalledPackageInfo], [SourcePackage])]
91 92
        pkgsInfo
            -- gather info for all packages
93 94
          | null pats = mergePackages (InstalledPackageIndex.allPackages installedPkgIndex)
                                      (         PackageIndex.allPackages sourcePkgIndex)
95 96

            -- gather info for packages matching search term
97 98
          | otherwise = mergePackages (matchingPackages InstalledPackageIndex.searchByNameSubstring installedPkgIndex)
                                      (matchingPackages (\ idx n -> concatMap snd (PackageIndex.searchByNameSubstring idx n)) sourcePkgIndex)
99 100 101

        matches :: [PackageDisplayInfo]
        matches = [ mergePackageInfo pref
102 103
                      installedPkgs sourcePkgs selectedPkg False
                  | (pkgname, installedPkgs, sourcePkgs) <- pkgsInfo
104 105
                  , not onlyInstalled || not (null installedPkgs)
                  , let pref        = prefs pkgname
106
                        selectedPkg = latestWithPref pref sourcePkgs ]
Lennart Kolmodin's avatar
Lennart Kolmodin committed
107

108 109
    if simpleOutput
      then putStr $ unlines
110
             [ display (pkgName pkg) ++ " " ++ display version
111 112 113 114
             | pkg <- matches
             , version <- if onlyInstalled
                            then              installedVersions pkg
                            else nub . sort $ installedVersions pkg
115
                                           ++ sourceVersions    pkg ]
116 117
             -- Note: this only works because for 'list', one cannot currently
             -- specify any version constraints, so listing all installed
118
             -- and source ones works.
119 120
      else
        if null matches
121
            then notice verbosity "No matches found."
122
            else putStr $ unlines (map showPackageSummaryInfo matches)
123 124 125
  where
    onlyInstalled = fromFlag (listInstalled listFlags)
    simpleOutput  = fromFlag (listSimpleOutput listFlags)
126

127
    matchingPackages search index =
128 129
      [ pkg
      | pat <- pats
130
      , pkg <- search index pat ]
131

132
info :: Verbosity
133
     -> PackageDBStack
134 135 136
     -> [Repo]
     -> Compiler
     -> ProgramConfiguration
137
     -> GlobalFlags
138
     -> InfoFlags
139
     -> [UserTarget]
140
     -> IO ()
refold's avatar
refold committed
141 142 143
info verbosity _ _ _ _ _ _ [] =
    notice verbosity "No packages requested. Nothing to do."

144 145 146
info verbosity packageDBs repos comp conf
     globalFlags _listFlags userTargets = do

147 148 149
    installedPkgIndex <- getInstalledPackages verbosity comp packageDBs conf
    sourcePkgDb   <- getSourcePackages    verbosity repos
    let sourcePkgIndex = packageIndex sourcePkgDb
150
        prefs name = fromMaybe anyVersion
151
                       (Map.lookup name (packagePreferences sourcePkgDb))
152 153 154

        -- Users may specify names of packages that are only installed, not
        -- just available source packages, so we must resolve targets using
155 156
        -- the combination of installed and source packages.
    let sourcePkgs' = PackageIndex.fromList
157 158
                    $ map packageId (InstalledPackageIndex.allPackages installedPkgIndex)
                   ++ map packageId (         PackageIndex.allPackages sourcePkgIndex)
159
    pkgSpecifiers <- resolveUserTargets verbosity
160 161
                       (fromFlag $ globalWorldFile globalFlags)
                       sourcePkgs' userTargets
162 163 164 165

    pkgsinfo      <- sequence
                       [ do pkginfo <- either die return $
                                         gatherPkgInfo prefs
166 167
                                           installedPkgIndex sourcePkgIndex
                                           pkgSpecifier
168 169 170 171 172 173
                            updateFileSystemPackageDetails pkginfo
                       | pkgSpecifier <- pkgSpecifiers ]

    putStr $ unlines (map showPackageDetailedInfo pkgsinfo)

  where
174 175 176 177 178
    gatherPkgInfo :: (PackageName -> VersionRange) ->
                     InstalledPackageIndex.PackageIndex ->
                     PackageIndex.PackageIndex SourcePackage ->
                     PackageSpecifier SourcePackage ->
                     Either String PackageDisplayInfo
179 180
    gatherPkgInfo prefs installedPkgIndex sourcePkgIndex (NamedPackage name constraints)
      | null (selectedInstalledPkgs) && null (selectedSourcePkgs)
181 182 183 184 185 186
      = Left $ "There is no available version of " ++ display name
            ++ " that satisfies "
            ++ display (simplifyVersionRange verConstraint)

      | otherwise
      = Right $ mergePackageInfo pref installedPkgs
Andres Löh's avatar
Andres Löh committed
187
                                 sourcePkgs  selectedSourcePkg'
188 189
                                 showPkgVersion
      where
190 191
        (pref, installedPkgs, sourcePkgs) =
          sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex
192

193
        selectedInstalledPkgs = InstalledPackageIndex.lookupDependency installedPkgIndex
194
                                    (Dependency name verConstraint)
195
        selectedSourcePkgs    =          PackageIndex.lookupDependency sourcePkgIndex
196
                                    (Dependency name verConstraint)
Andres Löh's avatar
Andres Löh committed
197
        selectedSourcePkg'    = latestWithPref pref selectedSourcePkgs
198 199 200 201 202

                         -- display a specific package version if the user
                         -- supplied a non-trivial version constraint
        showPkgVersion = not (null verConstraints)
        verConstraint  = foldr intersectVersionRanges anyVersion verConstraints
203
        verConstraints = [ vr | PackageConstraintVersion _ vr <- constraints ]
204

205 206
    gatherPkgInfo prefs installedPkgIndex sourcePkgIndex (SpecificSourcePackage pkg) =
        Right $ mergePackageInfo pref installedPkgs sourcePkgs
207 208 209 210
                                 selectedPkg True
      where
        name          = packageName pkg
        selectedPkg   = Just pkg
211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
        (pref, installedPkgs, sourcePkgs) =
          sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex

sourcePkgsInfo ::
  (PackageName -> VersionRange)
  -> PackageName
  -> InstalledPackageIndex.PackageIndex
  -> PackageIndex.PackageIndex SourcePackage
  -> (VersionRange, [Installed.InstalledPackageInfo], [SourcePackage])
sourcePkgsInfo prefs name installedPkgIndex sourcePkgIndex =
  (pref, installedPkgs, sourcePkgs)
  where
    pref          = prefs name
    installedPkgs = concatMap snd (InstalledPackageIndex.lookupPackageName installedPkgIndex name)
    sourcePkgs    =                         PackageIndex.lookupPackageName sourcePkgIndex name
226

227

228 229 230 231
-- | The info that we can display for each package. It is information per
-- package name and covers all installed and avilable versions.
--
data PackageDisplayInfo = PackageDisplayInfo {
232 233
    pkgName           :: PackageName,
    selectedVersion   :: Maybe Version,
234
    selectedSourcePkg :: Maybe SourcePackage,
235
    installedVersions :: [Version],
236
    sourceVersions    :: [Version],
237
    preferredVersions :: VersionRange,
238
    homepage          :: String,
239 240
    bugReports        :: String,
    sourceRepo        :: String,
241
    synopsis          :: String,
242 243 244 245 246
    description       :: String,
    category          :: String,
    license           :: License,
    author            :: String,
    maintainer        :: String,
247
    dependencies      :: [ExtDependency],
248
    flags             :: [Flag],
249 250
    hasLib            :: Bool,
    hasExe            :: Bool,
251 252 253 254
    executables       :: [String],
    modules           :: [ModuleName],
    haddockHtml       :: FilePath,
    haveTarball       :: Bool
255
  }
256

257
showPackageSummaryInfo :: PackageDisplayInfo -> String
258
showPackageSummaryInfo pkginfo =
259
  renderStyle (style {lineLength = 80, ribbonsPerLine = 1}) $
260
     char '*' <+> disp (pkgName pkginfo)
261
     $+$
262
     (nest 4 $ vcat [
263
       maybeShow (synopsis pkginfo) "Synopsis:" reflowParagraphs
264
     , text "Default available version:" <+>
265
       case selectedSourcePkg pkginfo of
266
         Nothing  -> text "[ Not available from any configured repository ]"
267
         Just pkg -> disp (packageVersion pkg)
268 269 270 271 272 273
     , text "Installed versions:" <+>
       case installedVersions pkginfo of
         []  | hasLib pkginfo -> text "[ Not installed ]"
             | otherwise      -> text "[ Unknown ]"
         versions             -> dispTopVersions 4
                                   (preferredVersions pkginfo) versions
274
     , maybeShow (homepage pkginfo) "Homepage:" text
275
     , text "License: " <+> text (display (license pkginfo))
276 277
     ])
     $+$ text ""
278
  where
279 280
    maybeShow [] _ _ = empty
    maybeShow l  s f = text s <+> (f l)
281 282

showPackageDetailedInfo :: PackageDisplayInfo -> String
283
showPackageDetailedInfo pkginfo =
284
  renderStyle (style {lineLength = 80, ribbonsPerLine = 1}) $
285 286 287
   char '*' <+> disp (pkgName pkginfo)
            <>  maybe empty (\v -> char '-' <> disp v) (selectedVersion pkginfo)
            <+> text (replicate (16 - length (display (pkgName pkginfo))) ' ')
288
            <>  parens pkgkind
289 290
   $+$
   (nest 4 $ vcat [
291
     entry "Synopsis"      synopsis     hideIfNull     reflowParagraphs
292
   , entry "Versions available" sourceVersions
293 294 295 296 297 298
           (altText null "[ Not available from server ]")
           (dispTopVersions 9 (preferredVersions pkginfo))
   , entry "Versions installed" installedVersions
           (altText null (if hasLib pkginfo then "[ Not installed ]"
                                            else "[ Unknown ]"))
           (dispTopVersions 4 (preferredVersions pkginfo))
299 300
   , entry "Homepage"      homepage     orNotSpecified text
   , entry "Bug reports"   bugReports   orNotSpecified text
301
   , entry "Description"   description  hideIfNull     reflowParagraphs
302
   , entry "Category"      category     hideIfNull     text
303
   , entry "License"       license      alwaysShow     disp
304 305 306 307
   , entry "Author"        author       hideIfNull     reflowLines
   , entry "Maintainer"    maintainer   hideIfNull     reflowLines
   , entry "Source repo"   sourceRepo   orNotSpecified text
   , entry "Executables"   executables  hideIfNull     (commaSep text)
308 309 310
   , entry "Flags"         flags        hideIfNull     (commaSep dispFlag)
   , entry "Dependencies"  dependencies hideIfNull     (commaSep disp)
   , entry "Documentation" haddockHtml  showIfInstalled text
311 312 313
   , entry "Cached"        haveTarball  alwaysShow     dispYesNo
   , if not (hasLib pkginfo) then empty else
     text "Modules:" $+$ nest 4 (vcat (map disp . sort . modules $ pkginfo))
314 315 316
   ])
   $+$ text ""
  where
317 318
    entry fname field cond format = case cond (field pkginfo) of
      Nothing           -> label <+> format (field pkginfo)
319 320 321 322 323 324 325 326 327
      Just Nothing      -> empty
      Just (Just other) -> label <+> text other
      where
        label   = text fname <> char ':' <> padding
        padding = text (replicate (13 - length fname ) ' ')

    normal      = Nothing
    hide        = Just Nothing
    replace msg = Just (Just msg)
328

329 330 331 332 333 334
    alwaysShow = const normal
    hideIfNull v = if null v then hide else normal
    showIfInstalled v
      | not isInstalled = hide
      | null v          = replace "[ Not installed ]"
      | otherwise       = normal
335 336 337
    altText nul msg v = if nul v then replace msg else normal
    orNotSpecified = altText null "[ Not specified ]"

338 339 340 341 342
    commaSep f = Disp.fsep . Disp.punctuate (Disp.char ',') . map f
    dispFlag f = case flagName f of FlagName n -> text n
    dispYesNo True  = text "Yes"
    dispYesNo False = text "No"

343 344 345 346 347 348 349 350 351
    isInstalled = not (null (installedVersions pkginfo))
    hasExes = length (executables pkginfo) >= 2
    --TODO: exclude non-buildable exes
    pkgkind | hasLib pkginfo && hasExes        = text "programs and library"
            | hasLib pkginfo && hasExe pkginfo = text "program and library"
            | hasLib pkginfo                   = text "library"
            | hasExes                          = text "programs"
            | hasExe pkginfo                   = text "program"
            | otherwise                        = empty
352

353

354 355 356 357 358 359 360 361 362 363 364
reflowParagraphs :: String -> Doc
reflowParagraphs =
    vcat
  . intersperse (text "")                    -- re-insert blank lines
  . map (fsep . map text . concatMap words)  -- reflow paragraphs
  . filter (/= [""])
  . groupBy (\x y -> "" `notElem` [x,y])     -- break on blank lines
  . lines

reflowLines :: String -> Doc
reflowLines = vcat . map text . lines
365 366 367 368 369 370 371 372

-- | We get the 'PackageDisplayInfo' by combining the info for the installed
-- and available versions of a package.
--
-- * We're building info about a various versions of a single named package so
-- the input package info records are all supposed to refer to the same
-- package name.
--
373
mergePackageInfo :: VersionRange
374
                 -> [Installed.InstalledPackageInfo]
375 376
                 -> [SourcePackage]
                 -> Maybe SourcePackage
377
                 -> Bool
378
                 -> PackageDisplayInfo
379 380
mergePackageInfo versionPref installedPkgs sourcePkgs selectedPkg showVer =
  assert (length installedPkgs + length sourcePkgs > 0) $
381
  PackageDisplayInfo {
382
    pkgName           = combine packageName source
383 384 385
                                packageName installed,
    selectedVersion   = if showVer then fmap packageVersion selectedPkg
                                   else Nothing,
386
    selectedSourcePkg = sourceSelected,
387
    installedVersions = map packageVersion installedPkgs,
388
    sourceVersions    = map packageVersion sourcePkgs,
389 390
    preferredVersions = versionPref,

391
    license      = combine Source.license       source
392
                           Installed.license    installed,
393
    maintainer   = combine Source.maintainer    source
394
                           Installed.maintainer installed,
395
    author       = combine Source.author        source
396
                           Installed.author     installed,
397
    homepage     = combine Source.homepage      source
398
                           Installed.homepage   installed,
399
    bugReports   = maybe "" Source.bugReports source,
400
    sourceRepo   = fromMaybe "" . join
401 402 403 404
                 . fmap (uncons Nothing Source.repoLocation
                       . sortBy (comparing Source.repoKind)
                       . Source.sourceRepos)
                 $ source,
405
                    --TODO: installed package info is missing synopsis
406 407
    synopsis     = maybe "" Source.synopsis      source,
    description  = combine Source.description    source
408
                           Installed.description installed,
409
    category     = combine Source.category       source
410
                           Installed.category    installed,
411
    flags        = maybe [] Source.genPackageFlags sourceGeneric,
412 413
    hasLib       = isJust installed
                || fromMaybe False
414
                   (fmap (isJust . Source.condLibrary) sourceGeneric),
415
    hasExe       = fromMaybe False
416 417
                   (fmap (not . null . Source.condExecutables) sourceGeneric),
    executables  = map fst (maybe [] Source.condExecutables sourceGeneric),
418
    modules      = combine Installed.exposedModules installed
419 420
                           (maybe [] Source.exposedModules
                                   . Source.library) source,
421 422
    dependencies = combine (map (SourceDependency . simplifyDependency) . Source.buildDepends) source
                           (map InstalledDependency . Installed.depends) installed,
423 424
    haddockHtml  = fromMaybe "" . join
                 . fmap (listToMaybe . Installed.haddockHTMLs)
425
                 $ installed,
426
    haveTarball  = False
427 428
  }
  where
429
    combine f x g y  = fromJust (fmap f x `mplus` fmap g y)
430 431
    installed :: Maybe Installed.InstalledPackageInfo
    installed = latestWithPref versionPref installedPkgs
432

433
    sourceSelected
434
      | isJust selectedPkg = selectedPkg
435 436 437
      | otherwise          = latestWithPref versionPref sourcePkgs
    sourceGeneric = fmap packageDescription sourceSelected
    source        = fmap flattenPackageDescription sourceGeneric
438

439 440 441 442
    uncons :: b -> (a -> b) -> [a] -> b
    uncons z _ []    = z
    uncons _ f (x:_) = f x

443

444 445 446 447 448
-- | Not all the info is pure. We have to check if the docs really are
-- installed, because the registered package info lies. Similarly we have to
-- check if the tarball has indeed been fetched.
--
updateFileSystemPackageDetails :: PackageDisplayInfo -> IO PackageDisplayInfo
449
updateFileSystemPackageDetails pkginfo = do
450
  fetched   <- maybe (return False) (isFetched . packageSource)
451
                     (selectedSourcePkg pkginfo)
452 453 454 455 456
  docsExist <- doesDirectoryExist (haddockHtml pkginfo)
  return pkginfo {
    haveTarball = fetched,
    haddockHtml = if docsExist then haddockHtml pkginfo else ""
  }
457

458 459 460 461 462 463 464 465
latestWithPref :: Package pkg => VersionRange -> [pkg] -> Maybe pkg
latestWithPref _    []   = Nothing
latestWithPref pref pkgs = Just (maximumBy (comparing prefThenVersion) pkgs)
  where
    prefThenVersion pkg = let ver = packageVersion pkg
                           in (withinRange ver pref, ver)


466
-- | Rearrange installed and source packages into groups referring to the
467 468 469
-- same package by name. In the result pairs, the lists are guaranteed to not
-- both be empty.
--
470
mergePackages :: [Installed.InstalledPackageInfo]
471
              -> [SourcePackage]
472
              -> [( PackageName
473
                  , [Installed.InstalledPackageInfo]
474 475
                  , [SourcePackage] )]
mergePackages installedPkgs sourcePkgs =
476
    map collect
477
  $ mergeBy (\i a -> fst i `compare` fst a)
478 479
            (groupOn packageName installedPkgs)
            (groupOn packageName sourcePkgs)
480
  where
481 482 483
    collect (OnlyInLeft  (name,is)         ) = (name, is, [])
    collect (    InBoth  (_,is)   (name,as)) = (name, is, as)
    collect (OnlyInRight          (name,as)) = (name, [], as)
484 485 486 487 488

groupOn :: Ord key => (a -> key) -> [a] -> [(key,[a])]
groupOn key = map (\xs -> (key (head xs), xs))
            . groupBy (equating key)
            . sortBy (comparing key)
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547

dispTopVersions :: Int -> VersionRange -> [Version] -> Doc
dispTopVersions n pref vs =
         (Disp.fsep . Disp.punctuate (Disp.char ',')
        . map (\ver -> if ispref ver then disp ver else parens (disp ver))
        . sort . take n . interestingVersions ispref
        $ vs)
    <+> trailingMessage

  where
    ispref ver = withinRange ver pref
    extra = length vs - n
    trailingMessage
      | extra <= 0 = Disp.empty
      | otherwise  = Disp.parens $ Disp.text "and"
                               <+> Disp.int (length vs - n)
                               <+> if extra == 1 then Disp.text "other"
                                                 else Disp.text "others"

-- | Reorder a bunch of versions to put the most interesting / significant
-- versions first. A preferred version range is taken into account.
--
-- This may be used in a user interface to select a small number of versions
-- to present to the user, e.g.
--
-- > let selectVersions = sort . take 5 . interestingVersions pref
--
interestingVersions :: (Version -> Bool) -> [Version] -> [Version]
interestingVersions pref =
      map ((\ns -> Version ns []) . fst) . filter snd
    . concat  . Tree.levels
    . swizzleTree
    . reorderTree (\(Node (v,_) _) -> pref (Version v []))
    . reverseTree
    . mkTree
    . map versionBranch

  where
    swizzleTree = unfoldTree (spine [])
      where
        spine ts' (Node x [])     = (x, ts')
        spine ts' (Node x (t:ts)) = spine (Node x ts:ts') t

    reorderTree _ (Node x []) = Node x []
    reorderTree p (Node x ts) = Node x (ts' ++ ts'')
      where
        (ts',ts'') = partition p (map (reorderTree p) ts)

    reverseTree (Node x cs) = Node x (reverse (map reverseTree cs))

    mkTree xs = unfoldTree step (False, [], xs)
      where
        step (node,ns,vs) =
          ( (reverse ns, node)
          , [ (any null vs', n:ns, filter (not . null) vs')
            | (n, vs') <- groups vs ]
          )
        groups = map (\g -> (head (head g), map tail g))
               . groupBy (equating head)