packages feed

fedora-haskell-tools-1.2: fhpkg.hs

{-# LANGUAGE CPP #-}

-- |
-- Module      :  Packages
-- Copyright   :  (C) 2017-2019  Jens Petersen
--
-- Maintainer  :  Jens Petersen <petersen@fedoraproject.org>
-- Stability   :  alpha
--
-- Explanation: Cloning and pulling package git repos

-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.

#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,13,0))
#else
import Control.Applicative (optional, some, (<|>)
#if (defined(MIN_VERSION_base) && MIN_VERSION_base(4,8,0))
#else
                           , pure, (<$>), (<*>)
#endif
                           )
#endif
import Control.Monad (filterM, unless, when, (>=>))
import Control.Monad.Extra (mapMaybeM, unlessM, whenJustM)
import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.Char (isDigit)
import Data.Maybe
import Data.List
import Data.List.Split (splitOn)
import Distribution.Fedora.Branch (Branch(..), getLatestFedoraBranch,
                                   readBranch, showBranch)
import Network.HTTP.Simple
import Network.HTTP.Types
import SimplePrompt (promptEnter)
import System.Directory (doesDirectoryExist, doesFileExist,
                         getCurrentDirectory, getHomeDirectory,
#if (defined(MIN_VERSION_directory) && MIN_VERSION_directory(1,2,5))
                         listDirectory,
#else
                         getDirectoryContents,
#endif
                         setCurrentDirectory)
import System.FilePath ((</>), (<.>), takeFileName)
import System.IO (BufferMode(..), hIsTerminalDevice, hSetBuffering, stdout)
--import System.Posix.Env (getEnv)
import Text.CSV (parseCSV)

import SimpleCmd
#if !MIN_VERSION_simple_cmd(0,2,3)
import System.Exit
import qualified System.Process as P
#endif
import SimpleCmd.Git
import SimpleCmdArgs

import Paths_fedora_haskell_tools (version)
import RPM (buildRequires, haskellSrcPkgs, Package,
            repoquery, rpmspec, rqfnewline)
import Utils (checkPkgsGit, withCurrentDirectory)

#if (defined(MIN_VERSION_directory) && MIN_VERSION_directory(1,2,5))
#else
listDirectory :: FilePath -> IO [FilePath]
listDirectory path =
  filter f <$> getDirectoryContents path
  where f filename = filename /= "." && filename /= ".."
#endif

main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  simpleCmdArgs (Just version) "Fedora Haskell packages tool"
    "Fedora packages maintenance tool" $
    subcommands . sort $
    [
      Subcommand "clone-new" "clone new packages" $
      cloneNew <$> branchArg
    , Subcommand "cblrpm" "Run cblrpm command" $
      cblrpm <$> strOptionWith 'c' "cmd" "CMD" "command to execute" <*> branchArg <*> pkgArgs
    , Subcommand "count" "count number of packages" $
      (repoqueryHaskellPkgs False >=> (print . length)) <$> branchArg
    , Subcommand "depends" "cabal-depends" $
      repoAction False (Output cabalDepends) <$> branchArg <*> pkgArgs
    , Subcommand "hackage-upload" "upload Hackage distro data" $
      hackageUpload <$> branchArg <*> switchRefresh
    , Subcommand "hackage-compare" "compare with Hackage distro data" $
      hackageCompare <$> branchArg <*> switchRefresh
    -- more or less the same as 'pushed'
    , Subcommand "head-origin" "packages with head in sync with origin" $
      headOrigin <$> branchArg <*> pkgArgs
    , Subcommand "leaf" "list leaf packages (slow!)" $
      leaves <$> switchWith 'v' "deps" "show also deps" <*> branchArg <*> pkgArgs
    , Subcommand "list" "list packages that BR ghc-Cabal-devel" $
      (repoqueryHaskellPkgs False >=> putStrList) <$> branchArg
    , Subcommand "missing" "missing dependency source packages" $
      missingDeps <$> branchArg <*> pkgArgs
    , Subcommand "new" "unbuilt packages" $
      (newPackages >=> putStrList) <$> branchArg
    , Subcommand "old-packages" "packages not in repoquery" $
      oldPackages <$> branchArg <*> pkgArgs
    , Subcommand "stackage-compare" "compare with stackage" $
      stackageCompare <$> streamOpt Nothing <*> stackageOpts <*> branchArg <*> pkgArgs
    , Subcommand "pushed" "show git pushed packages" $
      pushed <$> branchArg <*> pkgArgs
    , Subcommand "refresh" "cabal-rpm refresh" $
      refresh <$> switchWith 'n' "dry-run" "Show patch but don't apply" <*> branchArg <*> pkgArgs
    , Subcommand "remaining" "remaining packages to be built in TAG" $
      remaining <$> switchWith 'c' "count" "show many packages left" <*> strArg "TAG" <*> pkgArgs
    , Subcommand "subpkgs" "list subpackages" $
      repoAction True (Header True (\ p -> rpmspec [] (Just "%{name}-%{version}") (p <.> "spec") >>= putStrList)) <$> branchArg <*> pkgArgs
    , Subcommand "subpackaged" "list subpackaged libraries" $
      subpackaged <$> switchWith 'V' "show-versions" "Show versions" <*> branchArg <*> pkgArgs
    , Subcommand "tagged" "list koji DIST tagged builds" $
      listTagged_ <$> switchWith 's' "short" "list packages not builds" <*> strArg "TAG"
    , Subcommand "unbranched" "packages without this branch" $
      unbranched <$> branchArg <*> pkgArgs
    , Subcommand "unpushed" "show unpushed commits" $
      unpushed <$> switchWith 's' "short" "no log" <*> branchArg <*> pkgArgs
    , Subcommand "update" "cabal-rpm update" $
      update <$> keepOpt <*> streamOpt (Just "old") <*> branchArg <*> pkgArgs
    ]
  where
    branchArg :: Parser Branch
    branchArg = argumentWith branchM "BRANCH"

    branchM :: ReadM Branch
    branchM = maybeReader readBranch

    pkgArgs = some (removeSuffix "/" <$> strArg "PKG...")

    switchRefresh = switchWith 'r' "refresh" "repoquery --refresh"

    streamOpt mdesc = strOptionalWith 's' "stream" "STACKAGESTREAM" (fromMaybe "" mdesc +-+ "Stackage stream [" ++ ltsStream ++ "]") ltsStream

    ltsStream :: String
    ltsStream = "lts-24"

    stackageOpts :: Parser StkgOpt
    stackageOpts =
      flagWith' StkgMissing 'm' "missing" "only list missing packages" <|>
      flagWith StkgAll StkgOnly 'o' "only" "only Stackage packages"

    keepOpt = switchWith 'k' "keep-going" "Keep going after an error"

data StkgOpt = StkgAll | StkgOnly | StkgMissing
  deriving Eq

putStrList :: [String] -> IO ()
putStrList =
  putStr . unlines

cloneNew :: Branch -> IO ()
cloneNew branch =
  newPackages branch >>= repoAction_ True False (return ()) branch

-- FIXME option only to show PVP X.Y bumps or X.Y.Z bumps
stackageCompare :: String -> StkgOpt -> Branch -> [Package] -> IO ()
stackageCompare stream opt =
  repoAction True (Header False compareStackage)
  where
    compareStackage :: Package -> IO ()
    compareStackage p = do
      nvr <- cmd "fedpkg" ["verrel"]
      let hkg = removePrefix "ghc-" p
      (ok,_out,err) <- cmdFull "stack" ["--resolver" , stream, "list", hkg] ""
      -- FIXME rename to mstkgver?
      let stkg =
            if ok then
              case filter (not . ("Selected resolver:" `isPrefixOf`)) (lines err) of
                [nv] -> Just (removePrefix (hkg ++ "-") nv)
                _ -> Nothing
            else Nothing
      let same = isJust stkg && (fromJust stkg ++ "-") `isInfixOf` nvr
      unless same $
        if opt == StkgMissing
        then when (isNothing stkg) $ putStrLn p
        else
          if isNothing stkg
          then unless (opt == StkgOnly) $
               putStrLn $ stream ++ " missing: " ++ removePrefix "ghc-" p
          else do
            putStrLn nvr
            putStrLn $ replicate (length (dropVerrel nvr) + 1) ' ' ++ fromJust stkg +-+ "(" ++ stream ++ ")"

hackageUpload :: Branch -> Bool -> IO ()
hackageUpload hackageBranch refreshData = do
  branched <- getLatestFedoraBranch
  unless (branched == hackageBranch) $
    promptEnter "Not latest branch!  Please Enter to continue anyway"
  csv <- repoqueryHackageCSV
  home <- getHomeDirectory
  [username, password] <- map B.pack . words <$> readFile (home </> ".fedora/hackage.auth")
  req <- setRequestBasicAuth username password .
         setRequestBodyLBS (BL.pack csv) .
         addRequestHeader hContentType (B.pack "text/csv") .
         setRequestMethod methodPut <$>
         parseRequestThrow "https://hackage.haskell.org/distro/Fedora/packages.csv"
  resp <- httpLbs req
  BL.putStrLn $ getResponseBody resp
  where
    repoqueryHackageCSV :: IO String
    repoqueryHackageCSV = do
      pkgs <- repoqueryHackages hackageBranch
      -- Hackage csv chokes on a final newline
      intercalate "\n" . sort . map (replace "\"ghc-" "\"")  . lines <$>
        repoquery hackageBranch (["--repo=fedora", "--repo=updates", "--latest-limit=1", "--qf=\"%{name}\",\"%{version}\",\"https://src.fedoraproject.org/rpms/%{source_name}\"" ++ rqfnewline] ++ ["--refresh" | refreshData] ++ pkgs)

hackageCompare :: Branch -> Bool -> IO ()
hackageCompare hackageBranch refreshData =
  repoqueryHackages hackageBranch >>=
  compareHackage hackageBranch
  where
    compareHackage :: Branch -> [Package] -> IO ()
    compareHackage dist pkgs' = do
      req <- addRequestHeader hContentType (B.pack "text/csv") <$>
             parseRequestThrow "https://hackage.haskell.org/distro/Fedora/packages.csv"
      hck <- getResponseBody <$> httpLbs req
      let hackage = sort . either (error "Malformed Hackage csv") (map mungeHackage) $ parseCSV "packages.csv" (BL.unpack hck)
      repoquery dist (["--repo=fedora", "--repo=updates", "--latest-limit=1", "--qf=%{name},%{version}" ++ rqfnewline] ++ ["--refresh" | refreshData] ++ pkgs') >>=
        compareSets True hackage . sort . map mungeRepo . lines

    mungeHackage :: [String] -> PkgVer
    mungeHackage [n,v,_] = PV n v
    mungeHackage _ = error "Malformed Hackage csv"

    mungeRepo :: String -> PkgVer
    mungeRepo s | ',' `elem` s =
                  let (p,v) = break (== ',') s in
                    PV (removePrefix "ghc-" p) (tail v)
                | otherwise = error "Malformed repoquery output"

    compareSets :: Bool -> [PkgVer] -> [PkgVer] -> IO ()
    compareSets _ [] [] = return ()
    compareSets _ [] (f:fs) = do
      putStrLn (show f +-+ "(new)")
      compareSets False [] fs
    compareSets all' (h:hs) [] = do
      when all' $ putStrLn ("Removed:" +-+ show h)
      compareSets all' hs []
    compareSets all' (h:hs) (f:fs) | h == f = compareSets all' hs fs
                                   | h < f = do
                                       when all' $ putStrLn $ "Removed:" +-+ show h
                                       compareSets all' hs (f:fs)
                                   | h > f = do
                                       putStrLn $ show f +-+ "(new)"
                                       compareSets all' (h:hs) fs
                                   | otherwise = do
                                       putStrLn $ pvPkg h ++ ":" +-+ pvVer h +-+ "->" +-+ pvVer f
                                       compareSets all' hs fs

headOrigin :: Branch -> [Package] -> IO ()
headOrigin branch =
  repoAction False (Header False gitHeadAtOrigin) branch
  where
    gitHeadAtOrigin :: Package -> IO ()
    gitHeadAtOrigin pkg = do
      -- use gitDiffQuiet
      same <- gitBool "diff" ["--quiet", "origin" </> showBranch branch ++ "..HEAD"]
      when same $ putStrLn pkg

-- FIXME does not take static requires into account
leaves :: Bool -> Branch -> [Package] -> IO ()
leaves verb =
  repoAction True (Header verb checkLeafPkg)
  where
    -- FIXME: make a dependency cache
    checkLeafPkg :: Package -> IO ()
    checkLeafPkg pkg = do
      dir <- takeFileName <$> getCurrentDirectory
      let branchdir = dir /= pkg
          top = if branchdir then "../.." else ".."
          spec = pkg <.> "spec"
      subpkgs <- rpmspec ["--builtrpms"] (Just "%{name}") spec
      allpkgs <- listDirectory top
      let other = map (\ p -> top </> p </> (if branchdir then dir else "") </> p <.> "spec") $ allpkgs \\ [pkg]
      found <- filterM (dependsOn subpkgs) other
      if null found
        then putStrLn pkg
        else when verb $ putStrList found
        where
          dependsOn :: [Package] -> Package -> IO Bool
          dependsOn subpkgs p = do
            file <- doesFileExist p
            if file
              then do
              deps <- buildRequires p
              return $ any (`elem` deps) subpkgs
              else return False

missingDeps :: Branch -> [Package] -> IO ()
missingDeps dist =
  repoAction True (Output checkForMissingDeps) dist
  where
    checkForMissingDeps :: Package -> IO String
    checkForMissingDeps pkg = do
      dir <- takeFileName <$> getCurrentDirectory
      let top = if dir == pkg then ".." else "../.."
          spec = pkg <.> "spec"
      hasSpec <- doesFileExist spec
      if hasSpec
        then do
        deps <- buildRequires (pkg <.> "spec") >>= haskellSrcPkgs top dist
        unlines <$> filterM (noPkgDir top) deps
        else putStrLn ("no " ++ pkg ++ ".spec file found!") >> return ""
        where
          noPkgDir :: FilePath -> Package -> IO Bool
          noPkgDir top dep =
            not <$> doesDirectoryExist (top </> dep)

oldPackages :: Branch -> [Package] -> IO ()
oldPackages branch pkgs = do
  repopkgs <- repoqueryHaskellPkgs True branch
  putStrList (pkgs \\ repopkgs)

unpushed :: Bool -> Branch -> [Package] -> IO ()
unpushed nolog branch =
  repoAction True (Header False gitLogOneLine) branch
  where
    gitLogOneLine :: Package -> IO ()
    gitLogOneLine pkg = do
      out <- git "log" ["origin" </> showBranch branch ++ "..HEAD", "--pretty=oneline"]
      unless (null out) $
        putStrLn $ pkg ++ if nolog then "" else (unwords . map replaceHash . words) out
        where
          replaceHash h = if length h /= 40 then h else ":"

pushed :: Branch -> [Package] -> IO ()
pushed branch =
  repoAction True (Header False checkPushed) branch
  where
    checkPushed :: Package -> IO ()
    checkPushed pkg = do
      out <- git "log" ["origin" </> showBranch branch ++ "..HEAD", "--pretty=oneline"]
      when (null out) $ putStrLn pkg

data PkgVer = PV { pvPkg :: String, pvVer :: String}
  deriving (Eq)

instance Show PkgVer
  where
    show (PV p v) = p ++ "-" ++ v

instance Ord PkgVer
  where
    compare (PV p _) (PV p' _) = compare p p'

replace :: Eq a => [a] -> [a] -> [a] -> [a]
replace a b s@(x:xs) =
  if a `isPrefixOf` s
  then b ++ replace a b (drop (length a) s)
  else x:replace a b xs
replace _ _ [] = []

repoqueryHaskellPkgs :: Bool -> Branch -> IO [Package]
repoqueryHaskellPkgs verbose branch = do
  when verbose $ do
    tty <- hIsTerminalDevice stdout
    when tty $ warning "Getting packages from repoquery"
  let repo = "fedora"
      updates = ["updates"] -- maybeToList $ distUpdates dist
  bin <- words <$> repoquery branch (["--repo=" ++ repo ++ "-source"] ++ ["--repo=" ++ u  ++ "-source" | u <- updates] ++ ["--qf=%{name}" ++ rqfnewline, "--whatrequires", "ghc-Cabal-*"])
  when (null bin) $ error "No packages using ghc-Cabal-devel found!"
  return $ sort $ filter (not . isGhcXY) $ nub bin

isGhcXY :: String -> Bool
isGhcXY p = let prefix = head (splitOn "-" p)
            in all (\c -> isDigit c || c == '.') (removePrefix "ghc" prefix)

repoqueryHackages :: Branch -> IO [Package]
repoqueryHackages branch = do
  srcs <- repoqueryHaskellPkgs False branch
  libs <- repoqueryHaskellLibs False
  let binsrcs = filter (not . ("ghc-" `isPrefixOf`)) srcs
      sublibs = libs \\ map ("ghc-" ++) binsrcs
  return $ sort $ nub (delete "haskell-platform" srcs ++ sublibs)
  where
    repoqueryHaskellLibs :: Bool -> IO [Package]
    repoqueryHaskellLibs verbose = do
      when verbose $ putStrLn "Getting libraries from repoquery"
      let repo = "fedora" -- distRepo branched dist
          updates = ["updates"]
      bin <- words <$> repoquery branch (["--repo=" ++ repo] ++ ["--repo=" ++ u | u <- updates] ++ ["--qf=%{name}" ++ rqfnewline, "--whatprovides", "libHS*-ghc*.so()(64bit)"])
      when (null bin) $ error "No libHS*.so providers found!"
      return $ sort $ filter ("ghc-" `isPrefixOf`) $ nub bin

pagureListGhcHaskell :: IO [String]
pagureListGhcHaskell = do
  ps <- cmdLines "pagure" ["list", "--namespace", "rpms", "ghc*"]
  return $ filter (not . isGhcXY) ps

newPackages :: Branch -> IO [Package]
newPackages branch = do
  ps <- repoqueryHaskellPkgs True branch
  pps <- pagureListGhcHaskell
  local <- listDirectory "."
  filterM (\ d -> not <$> doesFileExist (d </> "dead.package")) $ nub (pps ++ ps) \\ (local ++ ["Agda-stdlib", "ghc", "ghc-rpm-macros", "ghc-srpm-macros", "ghc-srpm-macros-epel", "haskell-platform"])

haveSshKey :: IO Bool
haveSshKey = do
  home <- getHomeDirectory
  doesFileExist $ home </> ".ssh/id_ed25519"

data Action = Output (Package -> IO String) | Header Bool (Package -> IO ())

showHeader :: Action -> Bool
showHeader (Header b _) = b
showHeader (Output _) = False

repoAction :: Bool -> Action -> Branch -> [Package] -> IO ()
repoAction _ _ _ [] = return ()
repoAction needsSpec action branch (pkg:rest) = do
  withCurrentDirectory "." $ do
    when (showHeader action) $
      putStrLn $ "\n==" +-+ pkg +-+ showBranch branch +-+ "=="
    -- muser <- getEnv "USER"
    haveSSH <- haveSshKey
    fileExists <- doesFileExist pkg
    if fileExists
      then error $ pkg +-+ "is a file"
      else do
      dirExists <- doesDirectoryExist pkg
      unless dirExists $
        cmd_ "fedpkg" $ ["clone"] ++ ["-a" | not haveSSH] ++ ["-b", showBranch branch, pkg]
      setCurrentDirectory pkg
      pkggit <- do
        gd <- isGitDir "."
        if gd
          then checkPkgsGit
          else return False
      unless pkggit $
        error $ "not a Fedora pkg git dir!:" +-+ pkg
      actual <- gitBranch
      when (showBranch branch /= actual) $
        whenJustM (cmdMaybe "fedpkg" ["switch-branch", showBranch branch]) $ \ out ->
        when (showHeader action) $ putStrLn out
      currentBranch <- gitBranch
      when (showBranch branch == currentBranch) $
        unlessM (doesFileExist "dead.package") $ do
          let spec = pkg <.> "spec"
          hasSpec <- doesFileExist spec
          -- FIXME: silence for cmds that only output package names (eg unpushed -s)
          when (not hasSpec && showHeader action) $
            putStrLn "No spec file!"
          unless (needsSpec && not hasSpec) $
            case action of
              Header _ act -> act pkg
              Output act -> do
                out <- act pkg
                unless (null out) $ do
                  putStrLn $ "\n==" +-+ pkg +-+ showBranch branch +-+ "=="
                  putStrLn out
  repoAction needsSpec action branch rest

-- io independent of package
repoAction_ :: Bool -> Bool -> IO () -> Branch -> [Package] -> IO ()
repoAction_ header needsSpec action =
  repoAction needsSpec (Header header (const action))

-- compareRawhide :: Package -> IO ()
-- compareRawhide p = do
--   let spec = p <.> "spec"
--   nvr <- removeDisttag . unwords <$> rpmspec ["--srpm"] (Just "%{name}-%{version}-%{release}") spec
--   nvr' <- withBranch "master" $ do
--     haveSpec <- doesFileExist spec
--     unless haveSpec $ cmdSilent "git" ["pull"]
--     removeDisttag . unwords <$> rpmspec ["--srpm"] (Just "%{name}-%{version}-%{release}") spec
--   if nvr == nvr'
--     then putStrLn nvr
--     else do
--     putStrLn nvr
--     putStrLn nvr'
--   putStrLn ""
--   where
--     removeDisttag = reverse . tail . dropWhile (/= '.') . reverse

isFromHackage :: Package -> IO Bool
isFromHackage pkg =
  grep_ "hackage.haskell.org/package/" $ pkg <.> "spec"

update :: Bool -> String -> Branch -> [Package] -> IO ()
update keep stream =
  repoAction True (Header True doUpdate)
  where
    doUpdate :: Package -> IO ()
    doUpdate pkg = do
      hckg <- isFromHackage pkg
      if hckg
        then do
        ok <- cmdBool "cabal-rpm" ["update", "-o", stream]
        unless (ok || keep) $
          error' $ "cabal-rpm update failed for: " ++ pkg
        else putStrLn "skipping since not hackage"

refresh ::Bool -> Branch -> [Package] -> IO ()
refresh dryrun =
  repoAction True (Header True refreshPkg)
  where
    refreshPkg :: Package -> IO ()
    refreshPkg pkg = do
      hckg <- isFromHackage pkg
      if hckg
        then cmd_ "cabal-rpm" $ "refresh" : ["--dry-run" | dryrun]
        else putStrLn "skipping since not hackage"

listTagged_ :: Bool -> String -> IO ()
listTagged_ short tag =
  listTagged short tag >>= putStrList

listTagged :: Bool -> String -> IO [String]
listTagged short tag = do
  builds <- map (head . words) <$> cmdLines "koji" ["list-tagged", "--quiet", tag]
  return $ nub $ map (if short then dropVerrel else id) builds

dropVerrel :: String -> String
dropVerrel nvr =
  let parts = splitOn "-" nvr in
    intercalate "-" $ take (length parts - 2) parts

remaining :: Bool -> String -> [Package] -> IO ()
remaining count tag pkgs = do
  built <- listTagged True tag
  let left = pkgs \\ built
  if count
    then print $ length left
    else cmd_ "rpmbuild-order" $ ["sort", "-p"] ++ left

cabalDepends :: Package -> IO String
cabalDepends p = do
  hckg <- isFromHackage p
  if hckg then do
    vr <- removePrefix "ghc-" . head <$>
      rpmspec ["--srpm"] (Just "%{name}-%{version}") (p <.> "spec")
    setCurrentDirectory vr
    cmdQuiet "cabal-depends" ["--not-build", "--unique"]
    else return ""

cblrpm :: String -> Branch -> [Package] -> IO ()
cblrpm "" = error "CMD string must be given"
cblrpm cs =
  repoAction True (Header True doCblRpm)
  where
    doCblRpm :: Package -> IO ()
    doCblRpm p = do
    hckg <- isFromHackage p
    when hckg $
      cmd_ "cblrpm" [cs]

unbranched :: Branch -> [Package] -> IO ()
unbranched branch =
  mapM_ checkBranch
  where
    checkBranch :: Package -> IO ()
    checkBranch pkg =
      withCurrentDirectory pkg $ do
      dead <- doesFileExist "dead.package"
      unless dead $ do
        havebranch <- gitBool "show-ref" ["--verify", "--quiet", "refs/heads/" ++ showBranch branch]
        unless havebranch $ do
          remotebranch <- gitBool "ls-remote" ["--exit-code", "--refs", "origin", showBranch branch]
          unless remotebranch $
            putStrLn pkg

subpackaged ::Bool -> Branch -> [Package] -> IO ()
subpackaged versions branch pkgs = do
  repoAction True (Output listSubpkgs) branch pkgs
  where
    listSubpkgs pkg = do
      msubpkgs <- fmap (drop 2 . words) <$> cmdMaybe "grep" ["%global subpkgs", pkg <.> "spec"]
      case msubpkgs of
        Nothing -> return ""
        Just subpkgs ->
          intercalate "\n" <$> mapMaybeM expand subpkgs
      where
        expand subpkg =
          let macro = (init . drop 2) subpkg in
            fmap (removeVersion . last . words) <$> cmdMaybe "grep" ["%global " ++ macro, pkg <.> "spec"]

        removeVersion nv =
          if versions then nv else init (dropWhileEnd (/= '-') nv)

#if !MIN_VERSION_simple_cmd(0,2,2)
-- | 'gitBool c args' runs git command and return result
gitBool :: String -- ^ git command
        -> [String] -- ^ arguments
        -> IO Bool -- ^ result
gitBool c args = do
  mout <- cmdMaybe "git" (c:args)
  return $ isJust mout
#endif

#if !MIN_VERSION_simple_cmd(0,2,3)
cmdFull :: String -> [String] -> String -> IO (Bool, String, String)
cmdFull c args input = do
  (ret, out, err) <- P.readProcessWithExitCode c args input
  return (ret == ExitSuccess, removeTrailingNewline out, removeTrailingNewline err)
  where
    removeTrailingNewline :: String -> String
    removeTrailingNewline "" = ""
    removeTrailingNewline str' =
      if last str' == '\n'
      then init str'
      else str'
#endif