diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
 # Changelog
 
+## 1.2.1 (2022-11-23)
+- 'install': if dnf install fails, include command in error message
+- 'override': check for kerberos ticket
+- 'override': new --list and --expire option subcommands
+- 'parallel': change "0 jobs left in layer" message to "end of layer"
+- 'parallel': print package header for merge
+- 'parallel': wait for sidetag update to transition to request testing
+- 'request-branches': check package owner and admins: drop unordered-containers
+- 'sidetags': ensure krb ticket
+- 'update-version': now commits sources too
+- 'update-version': prompt rather than warn if not updating rawhide
+- Common plural: use 'no' for zero
+- Git: fix conflictPrompt to handle long hashes correctly
+- Prompt: use show for unprintable characters
+- mergeBranch: print package branch header
+- new RpmBuild module for rpm/build related functions moved from Package
+- newerMergeable may now include the newer branch
+- require bugzilla-redhat-1.0.1 since b.r.c dropped Bug see_also field (juhp/hsbugzilla#18)
+- use logMsg when waiting for repo
+
 ## 1.2 (2022-11-12)
 - Bodhi: for template file prompt instead of using fedpkg update
 - Bodhi: updates need to be comma separated (#36, Otto Liljalaakso)
@@ -47,14 +67,12 @@
 - 'parallel': dryrun for merge and delay until bodhiSidetagUpdate
 - 'parallel': filter out rawhide from parallel branch package builds update (#36)
 - 'parallel': formatting - use +-+, plural, don't color "is already"
-- 'parallel': no longer prompt for sidetag removal
+- 'parallel': no longer offers sidetag removal
 - 'parallel': only write "in layer" if there are layers
 - 'parallel': print pkg brnch header before merging
-- 'parallel': sidetag prompt now needs 'yes' to delete
 - 'request-branches/repos': check 100 fedora-scm-requests for duplicate
 - 'request-branches': add --quiet to suppress "exists" messages
 - 'request-branches': add --recurse-from BRANCH
-- 'request-branches': add another fedpkg failure error
 - 'request-branches': do not fetch if remote branch known
 - 'request-branches': improve --quiet help
 - 'request-branches': output if exists, again
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
 Fedora developers use a lot of time building packages across releases
 and workflow for adding new packages, etc.
 fbrnch was made to help (semi-)automate common workflows to save time
-and effort.
+and effort, and avoid common some mistakes.
 
 fbrnch is distributed under the GPL license version 2 or later.
 
@@ -185,8 +185,8 @@
 unless using `--no-merge`
 (though you may prefer to run `fbrnch merge <branch> ...` first instead).
 
-Except for rawhide using a sidetag is required.
-If you have more than one branch active sidetag for a branch,
+Except for rawhide using a --sidetag or --target is required.
+If you have more than one active sidetag for a branch,
 you can select one using `--target`.
 
 After parallel building you can create a Bodhi update from the sidetag.
@@ -257,7 +257,7 @@
 
 ```
 $ fbrnch --version
-1.2
+1.2.1
 $ fbrnch --help
 Fedora branch building tool
 
@@ -332,7 +332,7 @@
 
 3. Then either:
 
-a) using stack >= 2.1: `stack install`
+a) using stack (probably 2.3 or later): `stack install`
 
 or
 
@@ -417,9 +417,9 @@
 and gradually more features, including some generic commands across packages
 inspired by the older fedora-haskell-tools project.
 
-I have given a couple of short talks about fbranch:
+I have given a couple of short talks about fbrnch:
 - Nest with Fedora: [youtube](https://www.youtube.com/watch?v=40kTBsA674U) and [slides](https://github.com/juhp/presentations/blob/master/fedora-nest-2020-fbrnch/fbrnch-nest.md)
-- Lightning talk at devconf.cz 2021: [youtube](https://www.youtube.com/watch?v=O2-6rDuPMRA&t=2s)
+- Lightning talk for devconf.cz 2021: [youtube](https://www.youtube.com/watch?v=O2-6rDuPMRA&t=2s)
 
 ## Contribute
 Bug reports, feedback, and pull requests are all highly appreciated.
diff --git a/fbrnch.cabal b/fbrnch.cabal
--- a/fbrnch.cabal
+++ b/fbrnch.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.0
 name:                fbrnch
-version:             1.2
+version:             1.2.1
 synopsis:            Fedora packager tool to build package branches
 description:
             fbrnch (fedora branch) is a convenient packaging tool for
@@ -32,8 +32,8 @@
 build-type:          Simple
 extra-doc-files:     CHANGELOG.md
                      README.md
-tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4,
-                     GHC == 8.6.5,  GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2
+tested-with:         GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5,
+                     GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.5
 
 source-repository head
   type:                git
@@ -93,6 +93,7 @@
                        Paths_fbrnch
                        Prompt
                        Repoquery
+                       RpmBuild
                        Types
 
   hs-source-dirs:      src
@@ -103,7 +104,7 @@
                        -- pretty-terminal depends on base >= 4.9 (ghc8)
                        base >= 4.9 && < 5,
                        bodhi,
-                       bugzilla-redhat >= 1.0,
+                       bugzilla-redhat >= 1.0.1,
                        bytestring,
                        config-ini,
                        copr-api,
@@ -129,8 +130,6 @@
                        text,
                        time,
                        typed-process >= 0.2.4.0,
-                       -- for aeson-1 compatibility:
-                       unordered-containers,
                        utf8-string,
                        unix,
                        xdg-basedir
diff --git a/src/Bodhi.hs b/src/Bodhi.hs
--- a/src/Bodhi.hs
+++ b/src/Bodhi.hs
@@ -162,6 +162,8 @@
                   warning "overriding update type with 'newpackage'"
                 putStrLn $ "Creating Bodhi Update for " ++ nvrs ++ ":"
                 -- FIXME check for Bodhi URL to confirm update
+                -- FIXME returns json error string if it exists:
+                -- {"status": "error", "errors": [{"location": "body", "name": "builds", "description": "Update for ghc9.2-9.2.5-14.fc36 already exists"}]}
                 cmd_ "bodhi" $ ["updates", "new", "--type", if isJust mreview then "newpackage" else show updateType, "--severity", show severity, "--request", "testing", "--notes", changelog, "--autokarma", "--autotime", "--close-bugs"] ++ bugs ++ [nvrs]
                 return True
         when updatedone $ do
diff --git a/src/Cmd/Build.hs b/src/Cmd/Build.hs
--- a/src/Cmd/Build.hs
+++ b/src/Cmd/Build.hs
@@ -17,6 +17,7 @@
 import Koji
 import Package
 import Prompt
+import RpmBuild (checkSourcesMatch)
 import Types
 
 data BuildOpts = BuildOpts
@@ -33,6 +34,7 @@
   , buildoptAllowDirty :: Bool
   }
 
+-- FIXME merge --from
 -- FIXME check bugs before building?
 -- FIXME --sidetag
 -- FIXME --sort
@@ -77,24 +79,25 @@
   gitMergeOrigin br
   newrepo <- initialPkgRepo
   tty <- isTty
-  (ancestor,unmerged) <- newerMergeable br
+  (ancestor,unmerged,mnewer) <- newerMergeable br
   -- FIXME if already built or failed, also offer merge
   merged <-
     case buildoptMerge opts of
       Just False -> return False
       Just True -> do
-        whenJustM (getNewerBranch br) $ \newer ->
-          mergeBranch (buildoptDryrun opts) True True False (ancestor,unmerged) newer br
+        whenJust mnewer $ \newer ->
+          mergeBranch (buildoptDryrun opts) True True False (Just pkg) (ancestor,unmerged) newer br
         return True
       Nothing ->
         if ancestor && (newrepo || tty)
         then do
-          whenJustM (getNewerBranch br) $ \newer ->
-            mergeBranch (buildoptDryrun opts) True False True (ancestor,unmerged) newer br
+          whenJust mnewer $ \newer ->
+            mergeBranch (buildoptDryrun opts) True False True (Just pkg) (ancestor,unmerged) newer br
           return True
         else do
           unless (br == Rawhide) $
-            putStrLn "newer branch cannot be merged"
+            whenJust mnewer $ \newer ->
+            putStrLn $ show newer +-+ "branch not mergeable"
           return False
   let spec = packageSpec pkg
   checkForSpecFile spec
@@ -114,7 +117,7 @@
       -- see mergeBranch for: unmerged == 1 (774b5890)
       if tty && (not merged || (newrepo && ancestor && length unmerged == 1))
         then do
-        refPrompt unpushed $ "Press Enter to push and build" ++ (if length unpushed > 1 then "; or give a ref to push" else "") ++ (if not newrepo then "; or 'no' to skip pushing" else "")
+        refPrompt unpushed $ "Press Enter to push and build" ++ (if length unpushed > 1 then "; or give ref to push" else "") ++ (if not newrepo then "; or 'no' to skip pushing" else "")
         else return $ Just $ commitRef $ head unpushed
   let dryrun = buildoptDryrun opts
   buildstatus <- maybeTimeout 30 $ kojiBuildStatus nvr
diff --git a/src/Cmd/Copr.hs b/src/Cmd/Copr.hs
--- a/src/Cmd/Copr.hs
+++ b/src/Cmd/Copr.hs
@@ -7,19 +7,20 @@
   )
 where
 
-import Branches
-import Common
-import Common.System
-import qualified Common.Text as T
-import Package
-import Types (Archs(..))
-
 import Data.Ini.Config
 import Network.HTTP.Query (lookupKey, lookupKey')
 import System.Environment.XDG.BaseDir (getUserConfigDir)
 import System.Time.Extra (sleep)
 import Web.Fedora.Copr (coprChroots, fedoraCopr)
 import Web.Fedora.Copr.API (coprGetBuild)
+
+import Branches
+import Common
+import Common.System
+import qualified Common.Text as T
+import Package
+import RpmBuild (generateSrpm)
+import Types (Archs(..))
 
 data BuildBy = SingleBuild | ValidateByRelease | ValidateByArch | BuildByRelease
   deriving (Eq)
diff --git a/src/Cmd/Install.hs b/src/Cmd/Install.hs
--- a/src/Cmd/Install.hs
+++ b/src/Cmd/Install.hs
@@ -13,7 +13,10 @@
 import Package
 import Prompt
 import Repoquery
+import RpmBuild
 
+-- FIXME --force removal of existing incompatible dependent packages
+-- FIXME --subpackage to specify subpackage(s) to install/add
 -- FIXME --ignore-uninstalled subpackages
 -- FIXME --skip-unavailable
 -- FIXME --check any/all of package installed
@@ -68,7 +71,7 @@
                 ps <- filterM (pkgInstalled . rpmName . readNVRA) rpms
                 return $ if null ps then rpms else ps
             if reinstall || mforceshort' == Just ForceBuild
-            then do
+              then do
               let reinstalls =
                     filter (\ f -> readNVRA f `elem` already) toinstalls
               unless (null reinstalls) $
@@ -76,11 +79,12 @@
               let remaining = filterDebug $ toinstalls \\ reinstalls
               unless (null remaining) $
                 sudo_ "/usr/bin/dnf" $ "install" : "-q" : "-y" : remaining
-            else do
-              ok <- cmdBool "sudo" $ "/usr/bin/dnf" : "install" : "-q" : "-y" : filterDebug toinstalls
+              else do
+              let command = "/usr/bin/dnf" : "install" : "-q" : "-y" : filterDebug toinstalls
+              ok <- cmdBool "sudo" command
               unless ok $
                 if wasbuilt
-                then error' "build failed to install"
+                then error' $ "error from:" +-+ unwords command
                 else do
                   prompt_ "Press Enter to rebuild package"
                   doInstallPkg (Just ForceBuild) spec rpms already
@@ -132,3 +136,11 @@
           if null older
             then putStrLn $ unPackage pkg
             else putStrLn $ " " ++ unPackage pkg
+
+nvraInstalled :: NVRA -> IO Bool
+nvraInstalled rpm =
+  cmdBool "rpm" ["--quiet", "-q", showNVRA rpm]
+
+pkgInstalled :: String -> IO Bool
+pkgInstalled pkg =
+  cmdBool "rpm" ["--quiet", "-q", pkg]
diff --git a/src/Cmd/Local.hs b/src/Cmd/Local.hs
--- a/src/Cmd/Local.hs
+++ b/src/Cmd/Local.hs
@@ -19,6 +19,7 @@
 import Common.System
 import Git
 import Package
+import RpmBuild
 
 localCmd :: Bool -> Bool -> Maybe ForceShort -> [BCond]
          -> (BranchesReq, [String]) -> IO ()
diff --git a/src/Cmd/Merge.hs b/src/Cmd/Merge.hs
--- a/src/Cmd/Merge.hs
+++ b/src/Cmd/Merge.hs
@@ -22,7 +22,7 @@
     runMergeBranch :: Package -> AnyBranch -> IO ()
     runMergeBranch _ (OtherBranch _) =
       error' "merge only defined for release branches"
-    runMergeBranch _pkg (RelBranch br) = do
+    runMergeBranch pkg (RelBranch br) = do
       exists <- gitSwitchBranch' False br
       when exists $ do
         mfrom' <- if isJust mfrom
@@ -35,7 +35,7 @@
             gitMergeOrigin br
           (ancestor,unmerged) <- mergeable from br
           unmerged' <- filterOutTrivial mnotrivial unmerged
-          mergeBranch dryrun False noprompt showall (ancestor,unmerged') from br
+          mergeBranch dryrun False noprompt showall (Just pkg) (ancestor,unmerged') from br
       where
         filterOutTrivial :: Maybe Natural -> [Commit] -> IO [Commit]
         filterOutTrivial Nothing cs = return cs
@@ -58,12 +58,13 @@
   gitMergeable (show from `notElem` locals) from
 
 -- FIXME return merged ref
-mergeBranch :: Bool -> Bool -> Bool -> Bool
+mergeBranch :: Bool -> Bool -> Bool -> Bool -> Maybe Package
             -> (Bool,[Commit]) -- (ancestor,unmerged)
             -> Branch -> Branch -> IO ()
-mergeBranch _ _ _ _ _ _ Rawhide = return ()
-mergeBranch _ _ _ _ (_,[]) _ _ = return ()
-mergeBranch dryrun build noprompt showall (True, unmerged) from br = do
+mergeBranch _ _ _ _ _ _ _ Rawhide = return ()
+mergeBranch _ _ _ _ _ (_,[]) _ _ = return ()
+mergeBranch dryrun build noprompt showall mpkg (True, unmerged) from br = do
+  whenJust mpkg $ flip putPkgBrnchHdr br
   isnewrepo <- initialPkgRepo
   putStrLn $ (if isnewrepo || noprompt then "Merging from" else "New commits in") ++ " " ++ show from ++ ":"
   displayCommits showall unmerged
@@ -76,7 +77,7 @@
     then return $ Just $ commitRef (head unmerged)
     else refPrompt unmerged ("Press Enter to merge " ++ show from ++
          (if build then " and build" else "") ++
-         (if length unmerged > 1 then "; or give a ref to merge" else "") ++
+         (if length unmerged > 1 then "; or give ref to merge" else "") ++
          "; or 'no' to skip merge")
   -- ensure still on same branch!
   gitSwitchBranch (RelBranch br)
@@ -86,7 +87,8 @@
       git_ "fetch" ["origin", show from ++ ":" ++ show from]
     unless dryrun $
       git_ "merge" ["--quiet", ref]
-mergeBranch dryrun build noprompt showall (False,unmerged) from br = do
+mergeBranch dryrun build noprompt showall mpkg (False,unmerged) from br = do
+  whenJust mpkg $ flip putPkgBrnchHdr br
   putStrLn $ show from ++ " branch is not directly mergeable:"
   displayCommits False unmerged
   putNewLn
@@ -96,7 +98,7 @@
     displayCommits showall unpushed
   mmerge <-
     if noprompt then return Nothing
-    else conflictPrompt unmerged $ "Press Enter to skip merge" ++ (if build then " and build" else "") ++ "; or give a ref or 'HEAD' to attempt merge"
+    else conflictPrompt unmerged $ "Press Enter to skip merge" ++ (if build then " and build" else "") ++ "; or give ref or 'HEAD' to attempt merge"
   -- ensure still on same branch!
   gitSwitchBranch (RelBranch br)
   whenJust mmerge $ \ ref ->
diff --git a/src/Cmd/Mock.hs b/src/Cmd/Mock.hs
--- a/src/Cmd/Mock.hs
+++ b/src/Cmd/Mock.hs
@@ -11,6 +11,7 @@
 import Common.System
 import Git
 import Package
+import RpmBuild (generateSrpm)
 
 data NoClean = NoCleanBefore | NoCleanAfter | NoCleanAll | MockShortCircuit
   deriving Eq
diff --git a/src/Cmd/Override.hs b/src/Cmd/Override.hs
--- a/src/Cmd/Override.hs
+++ b/src/Cmd/Override.hs
@@ -1,6 +1,15 @@
-module Cmd.Override (overrideCmd)
+{-# LANGUAGE OverloadedStrings #-}
+
+module Cmd.Override (
+  overrideCmd,
+  OverrideMode(..)
+  )
 where
 
+import Data.Aeson (Object)
+import Fedora.Bodhi (bodhiOverrides)
+import Network.HTTP.Query
+
 import Common
 import Common.System
 
@@ -9,11 +18,18 @@
 import Cmd.WaitRepo (waitrepoCmd, WaitFetch(WaitNoFetch))
 import Git
 import Koji
+import Krb (krbTicket)
 import Package
+import Prompt (yesno)
 
--- FIXME option to expire (all) overrides
-overrideCmd :: Bool -> Maybe Int -> Bool -> (BranchesReq, [String]) -> IO ()
-overrideCmd dryrun mduration nowait breqpkgs = do
+data OverrideMode = OverrideCreate | OverrideList | OverrideExpire
+  deriving Eq
+
+-- FIXME debug option?
+overrideCmd :: Bool -> OverrideMode -> Maybe Int -> Bool
+            -> (BranchesReq, [String]) -> IO ()
+overrideCmd dryrun OverrideCreate mduration nowait breqpkgs = do
+  krbTicket
   unless nowait $
     putStrLn "Overriding"
   withPackagesByBranches HeaderMay False cleanGitFetchActive AnyNumber overrideBranch breqpkgs
@@ -33,3 +49,38 @@
       unless (any (`elem` tags) [show br, show br ++ "-updates", show br ++ "-override"]) $
         unlessM (checkAutoBodhiUpdate br) $
         bodhiCreateOverride dryrun mduration nvr
+overrideCmd _dryrun OverrideList _mduration _nowait (_breq,pkgs) =
+  withPackages pkgs $
+  packageOverrides >=> mapM_ showOverride
+  where
+    showOverride override =
+      case (lookupKey "expired_date" override :: Maybe String) of
+        Just _expiry -> return ()
+        Nothing ->
+          whenJust (lookupKey "nvr" override) $ \nvr ->
+          putStrLn $ nvr +-+
+          fromMaybe "" (lookupKey "expiration_date" override :: Maybe String)
+overrideCmd _dryrun OverrideExpire _mduration _nowait (_breq,pkgs) =
+  withPackages pkgs $
+  packageOverrides >=> mapM_ expireOverride
+  where
+    expireOverride override =
+      case (lookupKey "expired_date" override :: Maybe String) of
+        Just _expired -> return ()
+        Nothing -> do
+          whenJust (lookupKey "nvr" override) $ \nvr -> do
+            ok <- yesno $ "Expire override" +-+ nvr
+            when ok $
+              cmd_ "bodhi" ["overrides", "edit", "--expire", nvr]
+
+withPackages :: [FilePath] -> (Package -> IO ()) -> IO ()
+withPackages pkgs act =
+  forM_ pkgs $ \pkgdir ->
+  withExistingDirectory pkgdir $
+  getPackageName "." >>= act
+
+packageOverrides :: Package -> IO [Object]
+packageOverrides pkg =
+  -- FIXME could filter by "releases" for Branch's
+  bodhiOverrides [makeItem "packages" (unPackage pkg),
+                  makeItem "expired" "0"]
diff --git a/src/Cmd/Parallel.hs b/src/Cmd/Parallel.hs
--- a/src/Cmd/Parallel.hs
+++ b/src/Cmd/Parallel.hs
@@ -1,16 +1,18 @@
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE CPP, OverloadedStrings #-}
 
 module Cmd.Parallel (
   parallelBuildCmd,
-  SideTagTarget(..)
   ) where
 
 import Common
 import Common.System
 
 import Control.Concurrent.Async
+import Data.Aeson (Object,Value(String))
+import qualified Data.Text as T
 import Distribution.RPM.Build.Order (dependencyLayers)
 import Fedora.Bodhi hiding (bodhiUpdate)
+import qualified Fedora.Bodhi as FedoraBodhi (bodhiUpdate)
 import System.Console.Pretty
 import System.Time.Extra (sleep)
 
@@ -22,6 +24,7 @@
 import Krb
 import Koji
 import Package
+import RpmBuild (checkSourcesMatch)
 import Prompt
 import Types
 
@@ -31,13 +34,16 @@
                        }
 type JobAsync = (String, Async JobDone)
 
+-- FIXME offer to untag overrides afterwards
+-- FIXME merge --from
 -- FIXME print koji url of failed process or use koji-tool
 -- FIXME option to build multiple packages over branches in parallel
 -- FIXME use --wait-build=NVR
 -- FIXME check sources as early as possible
--- FIXME --single-layer to build packages at once regardless
+-- FIXME --ignore-dependencies to build packages at once regardless
 -- FIXME time builds
 -- FIXME copy bodhi notes from another branch update
+-- FIXME support non-sidetag update for parallel packages
 parallelBuildCmd :: Bool -> Maybe Bool -> Int -> Maybe SideTagTarget
                  -> (Maybe UpdateType, UpdateSeverity)
                  -> (BranchesReq, [String]) -> IO ()
@@ -60,14 +66,16 @@
   when (isJust mtarget && length branches > 1) $
     error' "You can only specify target with one branch"
   case pkgs of
-    [] -> parallelBranches "." branches
+    [] -> getPackageName "." >>= parallelBranches branches
     [p] -> withExistingDirectory p $
-           parallelBranches p branches
+           getPackageName p >>= parallelBranches branches
     _ ->
       forM_ branches $ \rbr -> do
       forM_ pkgs $ \p ->
         when (mmerge /= Just False) $
-        withExistingDirectory p $ mergeNewerBranch rbr
+        withExistingDirectory p $ do
+        pkg <- getPackageName p
+        mergeNewerBranch (Just pkg) rbr
       allLayers <- dependencyLayers pkgs
       let layers = drop firstlayer allLayers
       when (isNothing msidetagTarget && length allLayers > 1) $
@@ -91,14 +99,13 @@
           bodhiSidetagUpdate rbr (map jobNvr nvrclogs) target $
           if null input then changelog else input
   where
-    parallelBranches :: FilePath -> [Branch] -> IO ()
-    parallelBranches pkgdir brs = do
+    parallelBranches :: [Branch] -> Package -> IO ()
+    parallelBranches brs pkg = do
       krbTicket
       currentbranch <- gitCurrentBranch
       putStrLn $ "= Building " ++ pluralException (length brs) "branch" "branches" ++ " in parallel:"
       putStrLn $ unwords $ map show brs
-      pkg <- getPackageName pkgdir
-      jobs <- mapM (setupBranch pkg) brs
+      jobs <- mapM setupBranch brs
       (failures,nvrclogs) <- timeIO $ watchJobs Nothing [] [] jobs
       -- switch back to the original branch
       when (length brs /= 1) $
@@ -108,27 +115,26 @@
       when (isNothing msidetagTarget) $ do
         let spec = packageSpec pkg
         bodhiUpdate dryrun mupdate Nothing False spec $
-          intercalate "," $
-          map jobNvr $
+          intercalate "," . map jobNvr $
           filter ((/= Rawhide) . jobBranch) nvrclogs
       where
         -- FIXME time jobs
-        setupBranch :: Package -> Branch -> IO JobAsync
-        setupBranch pkg br = do
+        setupBranch :: Branch -> IO JobAsync
+        setupBranch br = do
           putPkgBrnchHdr pkg br
           target <- targetMaybeSidetag dryrun br msidetagTarget
-          when (mmerge /= Just False) $ mergeNewerBranch br
+          when (mmerge /= Just False) $ mergeNewerBranch Nothing br
           job <- startBuild Nothing 0 False False target pkg br "." >>= async
           unless dryrun $ sleep 3
           return (show br,job)
 
-    mergeNewerBranch :: Branch -> IO ()
-    mergeNewerBranch br = do
+    mergeNewerBranch :: Maybe Package -> Branch -> IO ()
+    mergeNewerBranch mpkg br = do
       gitSwitchBranch (RelBranch br)
-      (ancestor,unmerged) <- newerMergeable br
+      (ancestor,unmerged,mnewer) <- newerMergeable br
       unless dryrun $
-        whenJustM (getNewerBranch br) $ \newer -> do
-        mergeBranch dryrun True (mmerge == Just True) False (ancestor,unmerged) newer br
+        whenJust mnewer $ \newer ->
+        mergeBranch dryrun False (mmerge == Just True) False mpkg (ancestor,unmerged) newer br
 
     -- FIXME time builds or layers
     parallelBuild :: String -> Branch -> (Int,[[String]])
@@ -186,7 +192,10 @@
         Nothing -> sleep 1 >> watchJobs mlayer fails results (jobs ++ [job])
         -- (nvr,changelog)
         Just (Right result) -> do
-          putStrLn $ plural (length jobs) "job" +-+ "left" +-+ maybe "" (\l ->  "in layer" +-+ show l) mlayer
+          putStrLn $
+            if null jobs
+            then "ending layer" +-+ maybe "" show mlayer
+            else plural (length jobs) "job" +-+ "left" +-+ maybe "" (\l ->  "in layer" +-+ show l) mlayer
           watchJobs mlayer fails (result:results) jobs
         Just (Left except) -> do
           print except
@@ -316,8 +325,7 @@
             then bodhiSidetagUpdate rbr nvrs sidetag notes
             else
             unlessM (checkAutoBodhiUpdate rbr) $ do
-            -- arguably we already received the Updateid from the above bodhi command,
-            -- but we query it here via nvr
+            -- FIXME get updateid from above bodhi command output
             res <- bodhiUpdates [makeItem "display_user" "0", makeItem "builds" (last nvrs)]
             case res of
               [] -> do
@@ -325,7 +333,27 @@
                 prompt_ "Press Enter to resubmit to Bodhi"
                 bodhiSidetagUpdate rbr nvrs sidetag notes
               [update] ->
-                case lookupKey "updateid" update :: Maybe String of
+                case lookupKey "updateid" update of
                   Nothing -> error' "could not determine Update id"
-                  Just _updateid -> return ()
+                  Just updateid -> do
+                    putStr "Waiting for sidetag update to transition to 'request testing'"
+                    sleep 80
+                    bodhiUpdateTestingRequesting 1 updateid update
               _ -> error' $ "impossible happened: more than one update found for " ++ last nvrs
+
+    bodhiUpdateTestingRequesting :: Double -> String -> Object -> IO ()
+    bodhiUpdateTestingRequesting retries updateid update =
+      if retries > 4
+      then error' "\nupdate still not in 'request testing' status"
+      else
+        case lookupKey "request" update of
+          Just (String request) ->
+            putStrLn $ "\nrequest:" +-+ T.unpack request
+          _ -> do
+            mupdate' <- FedoraBodhi.bodhiUpdate updateid
+            case mupdate' of
+              Just update' -> do
+                putChar '.'
+                sleep (retries * 10)
+                bodhiUpdateTestingRequesting (retries + 1) updateid update'
+              _ -> error' "\nfailed to get updated metadata"
diff --git a/src/Cmd/PkgReview.hs b/src/Cmd/PkgReview.hs
--- a/src/Cmd/PkgReview.hs
+++ b/src/Cmd/PkgReview.hs
@@ -20,6 +20,7 @@
 import Krb
 import Package
 import Prompt
+import RpmBuild
 
 data ScratchOption = ScratchBuild | ScratchTask Int | SkipScratch
   deriving Eq
diff --git a/src/Cmd/Prep.hs b/src/Cmd/Prep.hs
--- a/src/Cmd/Prep.hs
+++ b/src/Cmd/Prep.hs
@@ -10,6 +10,7 @@
 import Git
 import InterleaveOutput (cmdSilent')
 import Package
+import RpmBuild
 
 data PrepPre = PrepClone | PrepPull
   deriving Eq
diff --git a/src/Cmd/RequestBranch.hs b/src/Cmd/RequestBranch.hs
--- a/src/Cmd/RequestBranch.hs
+++ b/src/Cmd/RequestBranch.hs
@@ -5,12 +5,6 @@
   requestPkgBranches
   ) where
 
-#if MIN_VERSION_aeson(2,0,0)
-import qualified Data.Aeson.KeyMap as M
-#else
-import qualified Data.HashMap.Lazy as M
-import Data.Text (Text)
-#endif
 import Network.HTTP.Query (lookupKey')
 
 import Common
@@ -154,16 +148,13 @@
   case epkginfo of
     Left err -> error' err
     Right pkginfo -> do
-      -- FIXME exclude unprivileged roles
-      let access = fasid `elem` concat (lookupKeyElems "access_users" pkginfo)
+      let access = fasid `elem` (lookupOwnerAdmins pkginfo :: [String])
       unless access $
         warning $ "-" +-+ fasid +-+ "does not have access"
       return access
   where
-    lookupKeyElems k o =
-      lookupKey' k o ::
-#if MIN_VERSION_aeson(2,0,0)
-        M.KeyMap [String]
-#else
-        M.HashMap Text [String]
-#endif
+    lookupOwnerAdmins pkginfo =
+      let access = lookupKey' "access_users" pkginfo
+          owners = lookupKey' "owner" access
+          admins = lookupKey' "admin" access
+      in owners ++ admins
diff --git a/src/Cmd/Scratch.hs b/src/Cmd/Scratch.hs
--- a/src/Cmd/Scratch.hs
+++ b/src/Cmd/Scratch.hs
@@ -13,6 +13,7 @@
 import Git
 import Koji
 import Package
+import RpmBuild (generateSrpm)
 import Types (Archs(..),SideTagTarget)
 
 -- FIXME allow parallel targets
diff --git a/src/Cmd/SideTags.hs b/src/Cmd/SideTags.hs
--- a/src/Cmd/SideTags.hs
+++ b/src/Cmd/SideTags.hs
@@ -1,10 +1,11 @@
 module Cmd.SideTags (sideTagsCmd) where
 
-import Control.Monad.Extra (whenM)
 import SimpleCmd (cmd_)
 
 import Branches
+import Common
 import Koji
+import Krb (krbTicket)
 import Prompt (yesno)
 
 sideTagsCmd :: Bool -> [Branch] -> IO ()
@@ -13,6 +14,7 @@
     if null brs
     then kojiUserSideTags Nothing
     else concat <$> mapM (kojiUserSideTags . Just) brs
+  when remove krbTicket
   mapM_ (if remove then removeSideTag else putStrLn) sidetags
   where
     removeSideTag :: String -> IO ()
diff --git a/src/Cmd/Update.hs b/src/Cmd/Update.hs
--- a/src/Cmd/Update.hs
+++ b/src/Cmd/Update.hs
@@ -10,6 +10,7 @@
 import InterleaveOutput (cmdSilent')
 import Krb
 import Package
+import Prompt (prompt_)
 
 import Data.RPM.VerCmp
 
@@ -41,7 +42,7 @@
     updatePkg :: Maybe String -> Package -> AnyBranch -> IO ()
     updatePkg mver pkg br = do
       when (br /= RelBranch Rawhide) $
-        warning $ "Are you sure you want to update " ++ show br ++ "?\n"
+        prompt_ $ "Are you sure you want to update " ++ show br ++ "?"
       spec <- if allowHEAD
               then findSpecfile
               else localBranchSpecFile pkg br
@@ -59,33 +60,26 @@
             error' $ "spec version already bumped to " ++ curver
           when (curver == nver) $
             putStrLn $ "already new version " ++ curver
+      let moldnewver =
+            case mver of
+              Just nver -> Just (curver,nver)
+              Nothing ->
+                case map (last . words) vdiff of
+                  [old,new] -> Just (old,new)
+                  _ -> Nothing
       unless onlysources $ do
         let (oldver,newver) =
-              case mver of
-                Just nver -> (curver,nver)
-                Nothing ->
-                  case map (last . words) vdiff of
-                    [old,new] -> (old,new)
-                    _ -> error' "complex version change"
+              fromMaybe (error' "complex version change") moldnewver
         -- FIXME take epoch into account
         when (rpmVerCompare oldver newver == GT) $
           putStrLn $ "current" +-+ oldver +-+ "is newer!"
         putStrLn $ oldver ++ " ->\n" ++ newver
-        if curver /= newver
-          then do
+        when (curver /= newver) $ do
           editSpecField "Version" newver spec
           editSpecField "Release" "0%{?dist}" spec
-          cmd_ "rpmdev-bumpspec" ["-c", "update to " ++ newver, spec]
           -- FIXME should be sure sources exists for distgit
           whenM (doesFileExist "sources") $
             cmd_ "sed" ["-i", "/" ++ unPackage pkg ++ "-" ++ oldver ++ "./d", "sources"]
-          else do
-          versions <- changelogVersions spec
-          let missing = null versions || not ((newver ++ "-") `isPrefixOf` head versions)
-          when missing $ do
-            cmd_ "rpmdev-bumpspec" ["-c", "update to " ++ newver, spec]
-            -- FIXME need to commit after add sources
-            git_ "commit" ["-a", "-m", "update to " ++ newver]
       whenM isPkgGitSshRepo $ do
         -- FIXME forM_
         sources <- map sourceFieldFile <$> cmdLines "spectool" ["-S", spec]
@@ -105,10 +99,16 @@
           cmd_ "spectool" ["-g", "-S", spec]
         krbTicket
         cmd_ "fedpkg" $ "new-sources" : filter isArchiveFile sources
-        putStr "Prepping... "
-        cmdSilent' "rpmbuild" ["-bp", spec]
-        putStrLn "done"
-        -- FIXME git amend (if previous commit was update)
+      whenJust moldnewver $ \(_old,newver) -> do
+        versions <- changelogVersions spec
+        let missing = null versions || not ((newver ++ "-") `isPrefixOf` head versions)
+        when missing $ do
+          cmd_ "rpmdev-bumpspec" ["-c", "update to " ++ newver, spec]
+          git_ "commit" ["-a", "-m", "update to " ++ newver]
+      putStr "Prepping... "
+      cmdSilent' "rpmbuild" ["-bp", spec]
+      putStrLn "done"
+      -- FIXME git amend (if previous commit was update)
 
     sourceFieldFile :: String -> FilePath
     sourceFieldFile field =
@@ -142,3 +142,8 @@
 editSpecField :: String -> String -> FilePath -> IO ()
 editSpecField field new spec =
   cmd_ "sed" ["-i", "-e s/^\\(" ++ field ++ ":\\s\\+\\).*/\\1" ++ new ++ "/", spec]
+
+changelogVersions :: FilePath -> IO [String]
+changelogVersions spec = do
+  ns <- cmdLines "rpmspec" ["-q", "--srpm", "--qf", "%{changelogname}", spec]
+  return $ map (removePrefix "- " . dropWhile (/= '-')) ns
diff --git a/src/Cmd/WaitRepo.hs b/src/Cmd/WaitRepo.hs
--- a/src/Cmd/WaitRepo.hs
+++ b/src/Cmd/WaitRepo.hs
@@ -33,6 +33,6 @@
       let spec = packageSpec pkg
       nvr <- pkgNameVerRel' br spec
       target <- targetMaybeSidetag dryrun br msidetagTarget
-      putStrLn $ "waiting for " ++ nvr ++ " to appear in " ++ target
+      logMsg $ "waiting for " ++ nvr ++ " to appear in " ++ target
       timeIO $
         kojiWaitRepo dryrun True target nvr
diff --git a/src/Common.hs b/src/Common.hs
--- a/src/Common.hs
+++ b/src/Common.hs
@@ -14,7 +14,7 @@
   ) where
 
 import Control.Monad.Extra -- hiding (loop)
-import Data.List.Extra hiding (merge)
+import Data.List.Extra hiding (list, merge)
 import Data.Maybe
 
 #if !MIN_VERSION_base(4,11,0)
@@ -31,7 +31,7 @@
 pluralException i ns ps =
   mconcat
   [
-    show i,
+    if i == 0 then "no" else show i,
     " ",
     if i == 1 then ns else ps
   ]
diff --git a/src/Git.hs b/src/Git.hs
--- a/src/Git.hs
+++ b/src/Git.hs
@@ -90,16 +90,18 @@
       putStr pull
 
 -- FIXME maybe require local branch already here
-newerMergeable :: Branch -> IO (Bool,[Commit])
+newerMergeable :: Branch -> IO (Bool,[Commit],Maybe Branch)
 newerMergeable br =
   if br == Rawhide
-  then return (False,[])
+  then return (False,[],Nothing)
   else do
     mnewer <- getNewerBranch br
     locals <- localBranches True
     case mnewer of
-      Just newer -> gitMergeable (show newer `notElem` locals) newer
-      Nothing -> return (False,[])
+      Just newer -> do
+        (ancestor,commits) <- gitMergeable (show newer `notElem` locals) newer
+        return (ancestor, commits, Just newer)
+      Nothing -> return (False,[],Nothing)
 
 data Commit = Commit
               { commitRef :: String,
@@ -282,6 +284,7 @@
 data CommitOpt = CommitMsg String | CommitAmend
 
 -- FIXME select ref by number
+-- FIXME minimum length of hash
 refPrompt :: [Commit] -> String -> IO (Maybe String)
 refPrompt commits txt = do
   case map commitRef commits of
@@ -298,6 +301,7 @@
             Nothing -> refPrompt commits txt
 
 -- FIXME also include branch
+-- FIXME minimum length of hash
 conflictPrompt :: [Commit] -> String -> IO (Maybe String)
 conflictPrompt commits txt = do
   case map commitRef commits of
@@ -307,9 +311,9 @@
       if null ref
         then return Nothing
         else
-        if ref `elem` commitrefs
-        then return $ Just ref
-        else
-          if lower ref == "head"
-          then return $ Just c
-          else conflictPrompt commits txt
+        case find (ref `isPrefixOf`) commitrefs of
+          Just cref -> return $ Just cref
+          Nothing ->
+            if lower ref == "head"
+            then return $ Just c
+            else conflictPrompt commits txt
diff --git a/src/Koji.hs b/src/Koji.hs
--- a/src/Koji.hs
+++ b/src/Koji.hs
@@ -42,7 +42,7 @@
 import Common.System
 import Git
 import Krb
-import Package
+import Package (fedpkg, Package, unPackage)
 import Pagure
 import Prompt
 import Types
@@ -220,6 +220,13 @@
                     logMsg $ "Waiting for " ++ buildtag ++ " to have " ++ nvr
                   waitRepo buildtag mrepo
 
+    -- FIXME: obsolete by using NVR
+    -- n-v-r -> n
+    nameOfNVR :: String -> String
+    nameOfNVR = removeSeg . removeSeg
+      where
+        removeSeg = init . dropWhileEnd (/= '-')
+
 kojiTagArchs :: String -> IO [String]
 kojiTagArchs tag = do
   st <- Koji.getTag fedoraHub (Koji.InfoString tag) Nothing
@@ -266,7 +273,7 @@
             putStrLn out
             let sidetag =
                   init . dropWhileEnd (/= '\'') $ dropPrefix "Side tag '" out
-            putStrLn $ "waiting for " ++ sidetag ++ " repo"
+            logMsg $ "waiting for " ++ sidetag ++ " repo"
             unless dryrun $
               cmd_ "koji" ["wait-repo", sidetag]
             return sidetag
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -50,8 +50,9 @@
 import Common.System
 import Git (CommitOpt(..))
 import ListReviews
-import Package (ForceShort(..), BCond(..))
+import RpmBuild (ForceShort(..), BCond(..))
 import Paths_fbrnch (version)
+import Types (SideTagTarget(..))
 
 main :: IO ()
 main = do
@@ -116,6 +117,7 @@
     , Subcommand "override" "Tag builds into buildroot override in Koji" $
       overrideCmd
       <$> dryrunOpt
+      <*> overrideModeOpt
       <*> optional (optionWith auto 'd' "duration" "DAYS" "Number of days until expiry [default 4]")
       <*> switchWith 'w' "no-wait" "Skip waitrepo step"
       <*> branchesPackages
@@ -598,3 +600,8 @@
     waitfetchOpt =
       flagWith' WaitDirty 'k' "allow-dirty" "Allow unclean git repo" <|>
       flagWith WaitFetch WaitNoFetch 'F' "no-fetch" "Skip git fetch"
+
+    overrideModeOpt :: Parser OverrideMode
+    overrideModeOpt =
+      flagWith' OverrideList 'l' "list" "List active override(s)" <|>
+      flagWith OverrideCreate OverrideExpire 'X' "expire" "Expire override(s)"
diff --git a/src/Package.hs b/src/Package.hs
--- a/src/Package.hs
+++ b/src/Package.hs
@@ -1,7 +1,6 @@
 {-# LANGUAGE CPP #-}
 
 module Package (
-  builtRpms,
   clonePkg,
   CloneUser(..),
   fedpkg,
@@ -9,23 +8,13 @@
   checkForSpecFile,
   getChangelog,
   cleanChangelog,
-  changelogVersions,
   changeLogPrompt,
+  getBranchDist,
   getPackageName,
   getSummaryURL,
   findSpecfile,
   maybeFindSpecfile,
   localBranchSpecFile,
-  generateSrpm,
-  generateSrpm',
-  BCond(..),
-  ForceShort(..),
-  isShortCircuit,
-  buildRPMs,
-  installDeps,
-  installMissingMacros,
-  checkSourcesMatch,
-  getSources,
   putPkgHdr,
   putPkgBrnchHdr,
   putPkgAnyBrnchHdr,
@@ -50,23 +39,11 @@
   packageSpec,
   pkgNameVerRel,
   pkgNameVerRel',
-  buildRequires,
-  notInstalled,
-  nvraInstalled,
-  pkgInstalled,
-  equivNVR,
-  nameOfNVR
+  equivNVR
   ) where
 
-import Data.Char (isDigit)
-import Data.Either (partitionEithers)
-import Data.RPM
 import Distribution.Fedora hiding (Fedora,EPEL,EPELNext)
-import Network.HTTP.Directory (Manager, httpExists, httpManager)
 import SimpleCmd.Rpm
-import System.Console.Pretty
-import System.IO.Extra (withTempDir)
-import System.Posix.Files
 
 import Branches
 import Common
@@ -103,11 +80,6 @@
     userlog <- prompt $ "Press Enter to use above or input " ++ fromMaybe "change" mcontext ++ " summary now" ++ if isJust mcontext then "; or 'no' to skip update" else ""
     return $ if null userlog then clog else userlog
 
-changelogVersions :: FilePath -> IO [String]
-changelogVersions spec = do
-  ns <- cmdLines "rpmspec" ["-q", "--srpm", "--qf", "%{changelogname}", spec]
-  return $ map (removePrefix "- " . dropWhile (/= '-')) ns
-
 getChangelog :: FilePath -> IO [String]
 getChangelog spec = do
   autochangelog <- grep_ "^%autochangelog" spec
@@ -179,284 +151,6 @@
           Nothing -> error' $ "No spec file for: " ++ unPackage pkg
     else findSpecfile
 
-rpmEval :: String -> IO (Maybe String)
-rpmEval s = do
-  res <- cmd "rpm" ["--eval", s]
-  return $ if null res || res == s then Nothing else Just res
-
--- rpmEval' :: String -> IO String
--- rpmEval' s = do
---   mres <- rpmEval s
---   fromMaybe (error' (show s ++ " undefined!")) mres
-
-generateSrpm :: Maybe AnyBranch -> FilePath -> IO FilePath
-generateSrpm = generateSrpm' False
-
-generateSrpm' :: Bool -> Maybe AnyBranch -> FilePath -> IO FilePath
-generateSrpm' force mbr spec = do
-  srcs <- getSources spec
-  distopt <- case mbr of
-               Nothing -> return []
-               Just br -> do
-                 dist <- getBranchDist br
-                 return ["--define", "dist " ++ rpmDistTag dist]
-  msrcrpmdir <- rpmEval "%{_srcrpmdir}"
-  srpmfile <- cmd "rpmspec" $ ["-q", "--srpm"] ++ distopt ++ ["--qf", fromMaybe "" msrcrpmdir </> "%{name}-%{version}-%{release}.src.rpm", spec]
-  cwd <- getCurrentDirectory
-  let sourcediropt = ["--define", "_sourcedir " ++ cwd]
-      opts = distopt ++ sourcediropt
-  if force then
-    buildSrpm opts
-    else do
-    exists <- doesFileExist srpmfile
-    if not exists
-      then buildSrpm opts
-      else do
-      srpmTime <- getModificationTime srpmfile
-      fileTimes <- mapM getModificationTime (spec:srcs)
-      if any (srpmTime <) fileTimes
-        then buildSrpm opts
-        else do
-        -- pretty print with ~/
-        putStrLn $ srpmfile ++ " is up to date"
-        return srpmfile
-  where
-    buildSrpm opts = do
-      srpm <- last . words <$> cmd "rpmbuild" (opts ++ ["-bs", spec])
-      putStrLn $ "Created " ++ takeFileName srpm
-      return srpm
-
-data ForceShort = ForceBuild | ShortCompile | ShortInstall
-  deriving Eq
-
-isShortCircuit :: Maybe ForceShort -> Bool
-isShortCircuit ms =
-  case ms of
-    Just s -> s /= ForceBuild
-    Nothing -> False
-
-data BCond = BuildWith String | BuildWithout String
-
-instance Show BCond where
-  show (BuildWith s) = "--with=" ++ s
-  show (BuildWithout s) = "--without=" ++ s
-
--- FIXME create build.log
--- Note does not check if bcond changed
--- FIXME check tarball timestamp
-buildRPMs :: Bool -> Bool -> Bool -> Maybe ForceShort -> [BCond] -> [FilePath]
-          -> AnyBranch -> FilePath -> IO Bool
-buildRPMs quiet debug noclean mforceshort bconds rpms br spec = do
-  needBuild <-
-    if isJust mforceshort
-    then return True
-    else
-    ifM (not . and <$> mapM doesFileExist rpms)
-    (return True) $
-    do specTime <- getModificationTime spec
-       rpmTimes <- sort <$> mapM getModificationTime rpms
-       return $ specTime > head rpmTimes
-  if not needBuild then
-    putStrLn "Existing rpms are newer than spec file (use --force to rebuild)"
-    else do
-    installDeps True spec
-    void $ getSources spec
-    dist <- getBranchDist br
-    cwd <- getCurrentDirectory
-    let buildopt =
-          case mforceshort of
-            Just ShortCompile -> ["-bc", "--short-circuit"]
-            Just ShortInstall -> ["-bi", "--short-circuit"]
-            _ -> "-bb" : ["--noclean" | noclean]
-        sourcediropt = ["--define", "_sourcedir " ++ cwd]
-        args = sourcediropt ++ ["--define", "dist " ++ rpmDistTag dist] ++
-               buildopt ++ map show bconds ++ [spec]
-    date <- cmd "date" ["+%T"]
-    putStr $ date ++ " Building " ++ takeBaseName spec ++ " locally... "
-    ok <- do
-      rbr <- anyBranchToRelease br
-      nvr <- pkgNameVerRel' rbr spec
-      let buildlog = ".build-" ++ showNVRVerRel (readNVR nvr) <.> "log"
-      timeIO $
-        if not quiet || isShortCircuit mforceshort
-        then do
-          putNewLn
-          -- FIXME would like to have pipeOutErr
-          let buildcmd = unwords $ "rpmbuild" : map quoteArg args ++ "|&" : "tee" : [buildlog ++ " && exit ${PIPESTATUS[0]}"]
-          when debug $ putStrLn buildcmd
-          shellBool buildcmd
-        else do
-          let buildcmd = unwords $ "rpmbuild" : map quoteArg args ++ [">&", buildlog]
-          when debug $ putStrLn buildcmd
-          res <- shellBool buildcmd
-          if res
-            then putStrLn "done"
-            else cmd_ "tail" ["-n 100", buildlog]
-          return res
-    unless ok $
-      error' $ takeBaseName spec ++ " failed to build"
-  return needBuild
-  where
-    quoteArg :: String -> String
-    quoteArg cs =
-      if ' ' `elem` cs then '\'' : cs ++ "'" else cs
-
--- FIXME print unavailable deps
-installDeps :: Bool -> FilePath -> IO ()
-installDeps strict spec = do
-  missingdeps <- nub <$> (buildRequires spec >>= filterM notInstalled)
-  unless (null missingdeps) $ do
-    putStrLn $ "Running 'dnf install' " ++ unwords missingdeps
-    cmd_ "/usr/bin/sudo" $ "/usr/bin/dnf":"install": ["--skip-broken" | not strict] ++ ["--assumeyes"] ++ missingdeps
-
-installMissingMacros :: FilePath -> IO ()
-installMissingMacros spec = do
-  macros <- mapMaybeM needSrpmMacro srpmMacros
-  missing <- filterM notInstalled macros
-  unless (null missing) $
-    cmd_ "/usr/bin/sudo" $ ["/usr/bin/dnf", "install", "--assumeyes"] ++ missing
-  where
-    srpmMacros :: [(String,String)]
-    srpmMacros =
-      [("%gometa", "go-rpm-macros"),
-       ("fontpkgname", "fonts-rpm-macros"),
-       ("%cargo_prep", "rust-packaging")]
-
-    needSrpmMacro :: (String,String) -> IO (Maybe String)
-    needSrpmMacro (meta, macros) = do
-      contents <- readFile spec
-      return $ if meta `isInfixOf` contents then Just macros else Nothing
-
-checkSourcesMatch :: FilePath -> IO ()
-checkSourcesMatch spec = do
-  -- "^[Ss]ource[0-9]*:"
-  sourcefiles <- map (takeFileName . last . words) <$> cmdLines "spectool" [spec]
-  sources <- lines <$> readFile "sources"
-  gitfiles <- gitLines "ls-files" []
-  let missing = filter (\src -> isNothing (find (src `isInfixOf`) sources) &&
-                                src `notElem` gitfiles)
-                sourcefiles
-  unless (null missing) $ do
-    prompt_ $ color Red $ unwords missing ++ " not in sources, please fix"
-    checkOnBranch
-    checkSourcesMatch spec
-  mgr <- httpManager
-  let pkg = takeBaseName spec
-  mapM_ (checkLookasideCache mgr pkg) sources
-  where
-    checkLookasideCache :: Manager -> String -> String -> IO ()
-    checkLookasideCache mgr pkg source = do
-      let (file,url) =
-            case words source of
-              ("SHA512":('(':fileparen):"=":[hash]) ->
-                let file' = dropSuffix ")" fileparen
-                in (file', "https://src.fedoraproject.org/lookaside/pkgs" +/+ pkg +/+ file +/+ "sha512" +/+ hash +/+ file)
-              [hash,file'] ->
-                (file', "https://src.fedoraproject.org/lookaside/pkgs" +/+ pkg +/+ file +/+ "md5" +/+ hash +/+ file)
-              _ -> error' $ "invalid/unknown source:\n" ++ source
-      unlessM (httpExists mgr url) $ do
-        putStrLn $ url ++ " not found"
-        putStrLn $ "uploading " ++ file ++ " to lookaside source repo"
-        fedpkg_ "upload" [file]
-
-getSources :: FilePath -> IO [FilePath]
-getSources spec = do
-  -- FIXME fallback to ~/rpmbuild/SOURCES?
-  msrcdir <- do
-    cwd <- getCurrentDirectory
-    msourcedir <- rpmEval "%{_sourcedir}"
-    case msourcedir of
-      Nothing -> return Nothing
-      Just srcdir -> do
-        canon <- canonicalizePath srcdir
-        if canon == cwd
-          then return Nothing
-          else do
-          dir <- doesDirectoryExist srcdir
-          if dir
-            then return msourcedir
-            else return Nothing
-  isPkgGit <- isPkgGitRepo
-  (patches,srcs) <- partitionEithers . map sourceFieldFile
-                    <$> cmdLines "spectool" ["-a", spec]
-  forM_ srcs $ \ src -> do
-    exists <- doesFileExist src &&^ checkCompression src
-    inSrcdir <- doesSourceDirFileExist msrcdir src
-    unless exists $ do
-      if inSrcdir
-        then maybeSourceDir createLink msrcdir src
-        else do
-        uploaded <-
-          if isPkgGit then do
-            have_sources <- doesFileExist "sources"
-            if have_sources then
-              grep_ src "sources"
-              else return False
-          else return False
-        mfedpkg <- findExecutable "fedpkg"
-        if uploaded && isJust mfedpkg
-          then cmd_ "fedpkg" ["sources"]
-          else do
-          cmd_ "spectool" ["-g", "-S", spec]
-          unlessM (doesFileExist src) $
-            error' $ "download failed: " ++ src
-    unless inSrcdir $
-      whenJust msrcdir $ \srcdir ->
-      createLink src (srcdir </> src)
-  forM_ patches $ \patch ->
-    unlessM (doesFileExist patch) $ do
-    inSrcdir <- doesSourceDirFileExist msrcdir patch
-    if inSrcdir
-      then maybeSourceDir copyFile msrcdir patch
-      else do
-      cmd_ "spectool" ["-g", "-P", spec]
-      unlessM (doesFileExist patch) $
-        error' $ "missing patch: " ++ patch
-  return $ srcs ++ patches
-  where
-    sourceFieldFile :: String -> Either FilePath FilePath
-    sourceFieldFile field =
-      case word1 field of
-        (f,v) ->
-          -- rpmdevtools 9.3 (spectool always lists --all)
-          -- "Source0:" or "Patch1:"
-          (case lower (dropWhileEnd isDigit (init f)) of
-             "source" -> Right
-             "patch" -> Left
-             _ -> error' $! "illegal field: " ++ f)
-          $ takeFileName v
-
-    checkCompression :: FilePath -> IO Bool
-    checkCompression file =
-      case
-        case takeExtension file of
-          ".gz" -> Just "gzip"
-          ".tgz" -> Just "gzip"
-          ".bz2" -> Just "bzip2"
-          ".xz" -> Just "xz"
-          ".lz" -> Just "lzip"
-          ".zstd" -> Just "zstd"
-          _ -> Nothing
-      of
-        Just prog -> do
-          have <- findExecutable prog
-          when (isNothing have) $ do
-            putStrLn $ "Running 'dnf install' " ++ prog
-            cmd_ "/usr/bin/sudo" $ "/usr/bin/dnf":"install": ["--assumeyes", prog]
-          cmdBool prog ["-t", file]
-        Nothing -> return True
-
-    maybeSourceDir :: (FilePath -> FilePath -> IO ())
-                   -> Maybe FilePath -> FilePath -> IO ()
-    maybeSourceDir act mdir file =
-      whenJust mdir $ \dir ->
-      act (dir </> file) file
-
-    doesSourceDirFileExist :: Maybe FilePath -> FilePath -> IO Bool
-    doesSourceDirFileExist Nothing _ = return False
-    doesSourceDirFileExist (Just srcdir) file =
-      doesFileExist (srcdir </> file)
-
 withExistingDirectory :: FilePath -> IO a -> IO a
 withExistingDirectory dir act = do
   exists <- doesDirectoryExist dir
@@ -488,67 +182,6 @@
 packageSpec :: Package -> FilePath
 packageSpec pkg = unPackage pkg <.> "spec"
 
-data BrPkg = IsBr AnyBranch | Unknown String | IsPkg String
-  deriving Show
-
--- splitBranchesPkgs :: Bool -> Maybe BranchOpts -> Bool -> [String]
---                   -> IO ([AnyBranch], [String])
--- splitBranchesPkgs release mbrnchopts exists args = do
---   pkggit <- isPkgGitRepo
---   brPkgs <- mapM (toBrPkg pkggit) args
---   let (brs,pkgs) = brPkgsToBranchesPkgs brPkgs
---   return $ case mbrnchopts of
---     Just _ | brs /= [] -> error' "cannot specify branches with branch options"
---     _ -> (brs,pkgs)
---   where
---     toBrPkg :: Bool -> String -> IO BrPkg
---     toBrPkg gitdir str =
---       case anyBranch str of
---         rbr@(RelBranch _) -> return (IsBr rbr)
---         abr@(OtherBranch p) -> if release then return (IsPkg p)
---                else
---                  ifM (isPath str)
---                  (return $ IsPkg str) $
---                  if gitdir
---                  then return (IsBr abr)
---                  else return $ if exists
---                                then IsBr abr
---                                else IsPkg str
---       where
---         isPath :: FilePath -> IO Bool
---         isPath fp =
---           if ".spec" `isExtensionOf` fp
---           then do
---             exists' <- doesFileExist fp
---             unless exists' $ error' $ fp ++ " file not found"
---             return True
---           else do
---             exists' <- doesDirectoryExist fp
---             let ispath = '/' `elem` fp
---             when (not exists' && ispath) $
---               error' $ fp ++ " directory not found"
---             return exists'
-
---     brPkgsToBranchesPkgs :: [BrPkg] -> ([AnyBranch], [String])
---     brPkgsToBranchesPkgs brpkgs =
---       let (pbrs,ppkgs) = span isBranch brpkgs
---       in (map toBranch pbrs, map toPackage ppkgs)
---       where
---         isBranch :: BrPkg -> Bool
---         isBranch (IsBr _) = True
---         isBranch (Unknown _) = True
---         isBranch (IsPkg _) = False
-
---         toBranch :: BrPkg -> AnyBranch
---         toBranch (IsBr br) = br
---         toBranch (Unknown br) = OtherBranch br
---         toBranch (IsPkg p) = error' $ "can't map package to branch: " ++ p
-
---         toPackage :: BrPkg -> String
---         toPackage (IsPkg p) = p
---         toPackage (Unknown p) = p
---         toPackage (IsBr b) = error' $ "can't map branch to package: " ++ show b
-
 data GitOpts =
   GitOpts
   { gitOptClean :: Bool
@@ -741,61 +374,12 @@
     Nothing -> error' $ "rpmspec failed to parse " ++ spec
     Just nvr -> return nvr
 
-builtRpms :: AnyBranch -> FilePath -> IO [FilePath]
-builtRpms br spec = do
-  dist <- getBranchDist br
-  -- previously was "" for pkggit
-  rpmdir <- fromMaybe "" <$> rpmEval "%{_rpmdir}"
-  rpms <- rpmspec ["--builtrpms", "--define", "dist " ++ rpmDistTag dist] (Just (rpmdir </>  "%{arch}/%{name}-%{version}-%{release}.%{arch}.rpm")) spec
-  if null rpms
-    then error' $ spec ++ " does not seem to create any rpms"
-    else return rpms
-
 getBranchDist :: AnyBranch -> IO Dist
 getBranchDist (RelBranch br) = branchDist br
 getBranchDist (OtherBranch _) = systemBranch >>= branchDist
 
--- from fedora-haskell-tools
-buildRequires :: FilePath -> IO [String]
-buildRequires spec = do
-  autorelease <- grep_ " %autorelease" spec
-  dynbr <- grep_ "^%generate_buildrequires" spec
-  brs <- mapMaybe primary <$>
-    if dynbr
-    then do
-      installMissingMacros spec
-      withTempDir $ \tmpdir -> do
-        let srpmdiropt = ["--define", "_srcrpmdir" ++ tmpdir]
-        out <- cmdIgnoreErr "rpmbuild" (["-br", "--nodeps", spec] ++ srpmdiropt) ""
-        -- Wrote: /current/dir/SRPMS/name-version-release.buildreqs.nosrc.rpm
-        case words out of
-          [] -> error' $ spec +-+ "could not generate source rpm for dynamic buildrequires"
-          ws -> cmdLines "rpm" ["-qp", "--requires", last ws]
-    else
-      -- FIXME should resolve meta
-      rpmspec ["--buildrequires"] Nothing spec
-  return $ brs ++ ["rpmautospec" | autorelease]
-  where
-    primary dep =
-      case (head . words) dep of
-        '(':rest -> Just rest
-        d -> if "rpmlib(" `isPrefixOf` d
-             then Nothing
-             else Just d
-
-notInstalled :: String -> IO Bool
-notInstalled pkg =
-  not <$> cmdBool "rpm" ["--quiet", "-q", "--whatprovides", pkg]
-
-nvraInstalled :: NVRA -> IO Bool
-nvraInstalled rpm =
-  cmdBool "rpm" ["--quiet", "-q", showNVRA rpm]
-
-pkgInstalled :: String -> IO Bool
-pkgInstalled pkg =
-  cmdBool "rpm" ["--quiet", "-q", pkg]
-
 -- FIXME should be more strict about dist tag (eg .fcNN only)
+-- FIXME replace with NVR's?
 equivNVR :: String -> String -> Bool
 equivNVR nvr1 nvr2
   | nvr1 == nvr2 = True
@@ -811,13 +395,63 @@
            -- allow differing dist
            in length r1 == length r2 && r1' == r2'
 
--- FIXME: obsolete by using NVR
--- n-v-r -> n
-nameOfNVR :: String -> String
-nameOfNVR = removeSeg . removeSeg
-  where
-    removeSeg = init . dropWhileEnd (/= '-')
+-- data BrPkg = IsBr AnyBranch | Unknown String | IsPkg String
+--   deriving Show
 
--- FIXME: drop when rpm-nvr has nvrVerRel
-showNVRVerRel :: NVR -> String
-showNVRVerRel (NVR _n vr) = showVerRel vr
+-- splitBranchesPkgs :: Bool -> Maybe BranchOpts -> Bool -> [String]
+--                   -> IO ([AnyBranch], [String])
+-- splitBranchesPkgs release mbrnchopts exists args = do
+--   pkggit <- isPkgGitRepo
+--   brPkgs <- mapM (toBrPkg pkggit) args
+--   let (brs,pkgs) = brPkgsToBranchesPkgs brPkgs
+--   return $ case mbrnchopts of
+--     Just _ | brs /= [] -> error' "cannot specify branches with branch options"
+--     _ -> (brs,pkgs)
+--   where
+--     toBrPkg :: Bool -> String -> IO BrPkg
+--     toBrPkg gitdir str =
+--       case anyBranch str of
+--         rbr@(RelBranch _) -> return (IsBr rbr)
+--         abr@(OtherBranch p) -> if release then return (IsPkg p)
+--                else
+--                  ifM (isPath str)
+--                  (return $ IsPkg str) $
+--                  if gitdir
+--                  then return (IsBr abr)
+--                  else return $ if exists
+--                                then IsBr abr
+--                                else IsPkg str
+--       where
+--         isPath :: FilePath -> IO Bool
+--         isPath fp =
+--           if ".spec" `isExtensionOf` fp
+--           then do
+--             exists' <- doesFileExist fp
+--             unless exists' $ error' $ fp ++ " file not found"
+--             return True
+--           else do
+--             exists' <- doesDirectoryExist fp
+--             let ispath = '/' `elem` fp
+--             when (not exists' && ispath) $
+--               error' $ fp ++ " directory not found"
+--             return exists'
+
+--     brPkgsToBranchesPkgs :: [BrPkg] -> ([AnyBranch], [String])
+--     brPkgsToBranchesPkgs brpkgs =
+--       let (pbrs,ppkgs) = span isBranch brpkgs
+--       in (map toBranch pbrs, map toPackage ppkgs)
+--       where
+--         isBranch :: BrPkg -> Bool
+--         isBranch (IsBr _) = True
+--         isBranch (Unknown _) = True
+--         isBranch (IsPkg _) = False
+
+--         toBranch :: BrPkg -> AnyBranch
+--         toBranch (IsBr br) = br
+--         toBranch (Unknown br) = OtherBranch br
+--         toBranch (IsPkg p) = error' $ "can't map package to branch: " ++ p
+
+--         toPackage :: BrPkg -> String
+--         toPackage (IsPkg p) = p
+--         toPackage (Unknown p) = p
+--         toPackage (IsBr b) = error' $ "can't map branch to package: " ++ show b
diff --git a/src/Prompt.hs b/src/Prompt.hs
--- a/src/Prompt.hs
+++ b/src/Prompt.hs
@@ -30,8 +30,9 @@
   if all isPrint inp
     then return inp
     else do
-    warning $ "input rejected because of unprintable character(s): " ++
-      filter (not . isPrint) inp
+    warning $
+      "input rejected because of unprintable character(s): '" ++
+      show inp ++ "'"
     prompt s
 
 prompt_ :: String -> IO ()
diff --git a/src/RpmBuild.hs b/src/RpmBuild.hs
new file mode 100644
--- /dev/null
+++ b/src/RpmBuild.hs
@@ -0,0 +1,353 @@
+module RpmBuild (
+  builtRpms,
+  buildRPMs,
+  installDeps,
+  installMissingMacros,
+  buildRequires,
+  getSources,
+  generateSrpm,
+  generateSrpm',
+  BCond(..),
+  ForceShort(..),
+  isShortCircuit,
+  checkSourcesMatch,
+  notInstalled,
+  )
+where
+
+import Data.Char (isDigit)
+import Data.Either (partitionEithers)
+import Data.RPM
+import Distribution.Fedora hiding (Fedora,EPEL,EPELNext)
+import Network.HTTP.Directory (Manager, httpExists, httpManager)
+import SimpleCmd.Rpm
+import System.Console.Pretty
+import System.IO.Extra (withTempDir)
+import System.Posix.Files
+
+import Branches
+import Common
+import Common.System
+import Git
+import Package
+import Prompt (prompt_)
+
+builtRpms :: AnyBranch -> FilePath -> IO [FilePath]
+builtRpms br spec = do
+  dist <- getBranchDist br
+  -- previously was "" for pkggit
+  rpmdir <- fromMaybe "" <$> rpmEval "%{_rpmdir}"
+  rpms <- rpmspec ["--builtrpms", "--define", "dist " ++ rpmDistTag dist] (Just (rpmdir </>  "%{arch}/%{name}-%{version}-%{release}.%{arch}.rpm")) spec
+  if null rpms
+    then error' $ spec ++ " does not seem to create any rpms"
+    else return rpms
+
+rpmEval :: String -> IO (Maybe String)
+rpmEval s = do
+  res <- cmd "rpm" ["--eval", s]
+  return $ if null res || res == s then Nothing else Just res
+
+-- rpmEval' :: String -> IO String
+-- rpmEval' s = do
+--   mres <- rpmEval s
+--   fromMaybe (error' (show s ++ " undefined!")) mres
+
+getSources :: FilePath -> IO [FilePath]
+getSources spec = do
+  -- FIXME fallback to ~/rpmbuild/SOURCES?
+  msrcdir <- do
+    cwd <- getCurrentDirectory
+    msourcedir <- rpmEval "%{_sourcedir}"
+    case msourcedir of
+      Nothing -> return Nothing
+      Just srcdir -> do
+        canon <- canonicalizePath srcdir
+        if canon == cwd
+          then return Nothing
+          else do
+          dir <- doesDirectoryExist srcdir
+          if dir
+            then return msourcedir
+            else return Nothing
+  isPkgGit <- isPkgGitRepo
+  (patches,srcs) <- partitionEithers . map sourceFieldFile
+                    <$> cmdLines "spectool" ["-a", spec]
+  forM_ srcs $ \ src -> do
+    exists <- doesFileExist src &&^ checkCompression src
+    inSrcdir <- doesSourceDirFileExist msrcdir src
+    unless exists $ do
+      if inSrcdir
+        then maybeSourceDir createLink msrcdir src
+        else do
+        uploaded <-
+          if isPkgGit then do
+            have_sources <- doesFileExist "sources"
+            if have_sources then
+              grep_ src "sources"
+              else return False
+          else return False
+        mfedpkg <- findExecutable "fedpkg"
+        if uploaded && isJust mfedpkg
+          then cmd_ "fedpkg" ["sources"]
+          else do
+          cmd_ "spectool" ["-g", "-S", spec]
+          unlessM (doesFileExist src) $
+            error' $ "download failed: " ++ src
+    unless inSrcdir $
+      whenJust msrcdir $ \srcdir ->
+      createLink src (srcdir </> src)
+  forM_ patches $ \patch ->
+    unlessM (doesFileExist patch) $ do
+    inSrcdir <- doesSourceDirFileExist msrcdir patch
+    if inSrcdir
+      then maybeSourceDir copyFile msrcdir patch
+      else do
+      cmd_ "spectool" ["-g", "-P", spec]
+      unlessM (doesFileExist patch) $
+        error' $ "missing patch: " ++ patch
+  return $ srcs ++ patches
+  where
+    sourceFieldFile :: String -> Either FilePath FilePath
+    sourceFieldFile field =
+      case word1 field of
+        (f,v) ->
+          -- rpmdevtools 9.3 (spectool always lists --all)
+          -- "Source0:" or "Patch1:"
+          (case lower (dropWhileEnd isDigit (init f)) of
+             "source" -> Right
+             "patch" -> Left
+             _ -> error' $! "illegal field: " ++ f)
+          $ takeFileName v
+
+    checkCompression :: FilePath -> IO Bool
+    checkCompression file =
+      case
+        case takeExtension file of
+          ".gz" -> Just "gzip"
+          ".tgz" -> Just "gzip"
+          ".bz2" -> Just "bzip2"
+          ".xz" -> Just "xz"
+          ".lz" -> Just "lzip"
+          ".zstd" -> Just "zstd"
+          _ -> Nothing
+      of
+        Just prog -> do
+          have <- findExecutable prog
+          when (isNothing have) $ do
+            putStrLn $ "Running 'dnf install' " ++ prog
+            cmd_ "/usr/bin/sudo" $ "/usr/bin/dnf":"install": ["--assumeyes", prog]
+          cmdBool prog ["-t", file]
+        Nothing -> return True
+
+    maybeSourceDir :: (FilePath -> FilePath -> IO ())
+                   -> Maybe FilePath -> FilePath -> IO ()
+    maybeSourceDir act mdir file =
+      whenJust mdir $ \dir ->
+      act (dir </> file) file
+
+    doesSourceDirFileExist :: Maybe FilePath -> FilePath -> IO Bool
+    doesSourceDirFileExist Nothing _ = return False
+    doesSourceDirFileExist (Just srcdir) file =
+      doesFileExist (srcdir </> file)
+
+generateSrpm :: Maybe AnyBranch -> FilePath -> IO FilePath
+generateSrpm = generateSrpm' False
+
+generateSrpm' :: Bool -> Maybe AnyBranch -> FilePath -> IO FilePath
+generateSrpm' force mbr spec = do
+  srcs <- getSources spec
+  distopt <- case mbr of
+               Nothing -> return []
+               Just br -> do
+                 dist <- getBranchDist br
+                 return ["--define", "dist " ++ rpmDistTag dist]
+  msrcrpmdir <- rpmEval "%{_srcrpmdir}"
+  srpmfile <- cmd "rpmspec" $ ["-q", "--srpm"] ++ distopt ++ ["--qf", fromMaybe "" msrcrpmdir </> "%{name}-%{version}-%{release}.src.rpm", spec]
+  cwd <- getCurrentDirectory
+  let sourcediropt = ["--define", "_sourcedir " ++ cwd]
+      opts = distopt ++ sourcediropt
+  if force then
+    buildSrpm opts
+    else do
+    exists <- doesFileExist srpmfile
+    if not exists
+      then buildSrpm opts
+      else do
+      srpmTime <- getModificationTime srpmfile
+      fileTimes <- mapM getModificationTime (spec:srcs)
+      if any (srpmTime <) fileTimes
+        then buildSrpm opts
+        else do
+        -- pretty print with ~/
+        putStrLn $ srpmfile ++ " is up to date"
+        return srpmfile
+  where
+    buildSrpm opts = do
+      srpm <- last . words <$> cmd "rpmbuild" (opts ++ ["-bs", spec])
+      putStrLn $ "Created " ++ takeFileName srpm
+      return srpm
+
+data ForceShort = ForceBuild | ShortCompile | ShortInstall
+  deriving Eq
+
+isShortCircuit :: Maybe ForceShort -> Bool
+isShortCircuit ms =
+  case ms of
+    Just s -> s /= ForceBuild
+    Nothing -> False
+
+data BCond = BuildWith String | BuildWithout String
+
+instance Show BCond where
+  show (BuildWith s) = "--with=" ++ s
+  show (BuildWithout s) = "--without=" ++ s
+
+-- FIXME create build.log
+-- Note does not check if bcond changed
+-- FIXME check tarball timestamp
+buildRPMs :: Bool -> Bool -> Bool -> Maybe ForceShort -> [BCond] -> [FilePath]
+          -> AnyBranch -> FilePath -> IO Bool
+buildRPMs quiet debug noclean mforceshort bconds rpms br spec = do
+  needBuild <-
+    if isJust mforceshort
+    then return True
+    else
+    ifM (not . and <$> mapM doesFileExist rpms)
+    (return True) $
+    do specTime <- getModificationTime spec
+       rpmTimes <- sort <$> mapM getModificationTime rpms
+       return $ specTime > head rpmTimes
+  if not needBuild then
+    putStrLn "Existing rpms are newer than spec file (use --force to rebuild)"
+    else do
+    installDeps True spec
+    void $ getSources spec
+    dist <- getBranchDist br
+    cwd <- getCurrentDirectory
+    let buildopt =
+          case mforceshort of
+            Just ShortCompile -> ["-bc", "--short-circuit"]
+            Just ShortInstall -> ["-bi", "--short-circuit"]
+            _ -> "-bb" : ["--noclean" | noclean]
+        sourcediropt = ["--define", "_sourcedir " ++ cwd]
+        args = sourcediropt ++ ["--define", "dist " ++ rpmDistTag dist] ++
+               buildopt ++ map show bconds ++ [spec]
+    date <- cmd "date" ["+%T"]
+    putStr $ date ++ " Building " ++ takeBaseName spec ++ " locally... "
+    ok <- do
+      rbr <- anyBranchToRelease br
+      nvr <- pkgNameVerRel' rbr spec
+      let buildlog = ".build-" ++ (showVerRel . nvrVerRel . readNVR) nvr <.> "log"
+      timeIO $
+        if not quiet || isShortCircuit mforceshort
+        then do
+          putNewLn
+          -- FIXME would like to have pipeOutErr
+          let buildcmd = unwords $ "rpmbuild" : map quoteArg args ++ "|&" : "tee" : [buildlog ++ " && exit ${PIPESTATUS[0]}"]
+          when debug $ putStrLn buildcmd
+          shellBool buildcmd
+        else do
+          let buildcmd = unwords $ "rpmbuild" : map quoteArg args ++ [">&", buildlog]
+          when debug $ putStrLn buildcmd
+          res <- shellBool buildcmd
+          if res
+            then putStrLn "done"
+            else cmd_ "tail" ["-n 100", buildlog]
+          return res
+    unless ok $
+      error' $ takeBaseName spec ++ " failed to build"
+  return needBuild
+  where
+    quoteArg :: String -> String
+    quoteArg cs =
+      if ' ' `elem` cs then '\'' : cs ++ "'" else cs
+
+-- FIXME print unavailable deps
+installDeps :: Bool -> FilePath -> IO ()
+installDeps strict spec = do
+  missingdeps <- nub <$> (buildRequires spec >>= filterM notInstalled)
+  unless (null missingdeps) $ do
+    putStrLn $ "Running 'dnf install' " ++ unwords missingdeps
+    cmd_ "/usr/bin/sudo" $ "/usr/bin/dnf":"install": ["--skip-broken" | not strict] ++ ["--assumeyes"] ++ missingdeps
+
+installMissingMacros :: FilePath -> IO ()
+installMissingMacros spec = do
+  macros <- mapMaybeM needSrpmMacro srpmMacros
+  missing <- filterM notInstalled macros
+  unless (null missing) $
+    cmd_ "/usr/bin/sudo" $ ["/usr/bin/dnf", "install", "--assumeyes"] ++ missing
+  where
+    srpmMacros :: [(String,String)]
+    srpmMacros =
+      [("%gometa", "go-rpm-macros"),
+       ("fontpkgname", "fonts-rpm-macros"),
+       ("%cargo_prep", "rust-packaging")]
+
+    needSrpmMacro :: (String,String) -> IO (Maybe String)
+    needSrpmMacro (meta, macros) = do
+      contents <- readFile spec
+      return $ if meta `isInfixOf` contents then Just macros else Nothing
+
+-- from fedora-haskell-tools
+buildRequires :: FilePath -> IO [String]
+buildRequires spec = do
+  autorelease <- grep_ " %autorelease" spec
+  dynbr <- grep_ "^%generate_buildrequires" spec
+  brs <- mapMaybe primary <$>
+    if dynbr
+    then do
+      installMissingMacros spec
+      withTempDir $ \tmpdir -> do
+        let srpmdiropt = ["--define", "_srcrpmdir" ++ tmpdir]
+        out <- cmdIgnoreErr "rpmbuild" (["-br", "--nodeps", spec] ++ srpmdiropt) ""
+        -- Wrote: /current/dir/SRPMS/name-version-release.buildreqs.nosrc.rpm
+        case words out of
+          [] -> error' $ spec +-+ "could not generate source rpm for dynamic buildrequires"
+          ws -> cmdLines "rpm" ["-qp", "--requires", last ws]
+    else
+      -- FIXME should resolve meta
+      rpmspec ["--buildrequires"] Nothing spec
+  return $ brs ++ ["rpmautospec" | autorelease]
+  where
+    primary dep =
+      case (head . words) dep of
+        '(':rest -> Just rest
+        d -> if "rpmlib(" `isPrefixOf` d
+             then Nothing
+             else Just d
+
+checkSourcesMatch :: FilePath -> IO ()
+checkSourcesMatch spec = do
+  -- "^[Ss]ource[0-9]*:"
+  sourcefiles <- map (takeFileName . last . words) <$> cmdLines "spectool" [spec]
+  sources <- lines <$> readFile "sources"
+  gitfiles <- gitLines "ls-files" []
+  let missing = filter (\src -> isNothing (find (src `isInfixOf`) sources) &&
+                                src `notElem` gitfiles)
+                sourcefiles
+  unless (null missing) $ do
+    prompt_ $ color Red $ unwords missing ++ " not in sources, please fix"
+    checkOnBranch
+    checkSourcesMatch spec
+  mgr <- httpManager
+  let pkg = takeBaseName spec
+  mapM_ (checkLookasideCache mgr pkg) sources
+  where
+    checkLookasideCache :: Manager -> String -> String -> IO ()
+    checkLookasideCache mgr pkg source = do
+      let (file,url) =
+            case words source of
+              ("SHA512":('(':fileparen):"=":[hash]) ->
+                let file' = dropSuffix ")" fileparen
+                in (file', "https://src.fedoraproject.org/lookaside/pkgs" +/+ pkg +/+ file +/+ "sha512" +/+ hash +/+ file)
+              [hash,file'] ->
+                (file', "https://src.fedoraproject.org/lookaside/pkgs" +/+ pkg +/+ file +/+ "md5" +/+ hash +/+ file)
+              _ -> error' $ "invalid/unknown source:\n" ++ source
+      unlessM (httpExists mgr url) $ do
+        putStrLn $ url ++ " not found"
+        putStrLn $ "uploading " ++ file ++ " to lookaside source repo"
+        fedpkg_ "upload" [file]
+
+notInstalled :: String -> IO Bool
+notInstalled pkg =
+  not <$> cmdBool "rpm" ["--quiet", "-q", "--whatprovides", pkg]
