packages feed

fbrnch-1.8: src/Cmd/BranchLogs.hs

{-# LANGUAGE CPP #-}

-- FIXME rename to BranchLog?
module Cmd.BranchLogs (
  branchLogCmd
  )
where

import Control.Monad (forM_, unless, when)
import Data.Function (on)
#if !MIN_VERSION_base(4,18,0)
import qualified Data.List as List
import qualified Data.Foldable as Foldable
#endif
import Data.List.Extra (dropPrefix, find, splitOn, stripInfix, uncons)
import Data.List.NonEmpty (NonEmpty(..))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (mapMaybe)
import Distribution.Fedora.Branch (readBranch)
import System.Console.Pretty (color, Color(..), supportsPretty)

import Branches
import Common.System
import Git (gitSwitchBranch)
import Package

-- FIXME --all-commits
-- FIXME select latest branches (local and remote)
-- FIXME how to handle dirty
-- FIXME handle detached head
branchLogCmd :: Bool -> Bool -> Bool -> (BranchesReq,[String]) -> IO ()
branchLogCmd latest allbrs nosimplydecor (breq, pkgs) = do
  colored <- supportsPretty
  if null pkgs
    then logPkg colored "."
    else mapM_ (logPkg colored) pkgs
--  withPackagesMaybeBranch (if length pkgs > 1 then HeaderMust else HeaderNone) False Nothing (logPkg colored active) (breq, packages)
 where
   logPkg :: Bool -> FilePath -> IO ()
   logPkg colored path =
     withExistingDirectory path $ do
     -- FIXME handle inactive branch automatically (without --inactive or error)
     branches <- listOfBranches True (not allbrs) breq
     let listBranches' =
           if allbrs then listAllBranches else listBranches
     locals <- listBranches' True
     remotes <- listBranches' False
     current <- gitCurrentBranch
     forM_ branches $ \br ->
       unless (br `elem` locals) $
       if br `elem` remotes
       then
         gitSwitchBranch (RelBranch br)
       else error' $ "branch does not exist:" +-+ showBranch br
     gitSwitchBranch current
     if latest
       then latestBranches branches
       else do
       forM_ branches $ \br -> do
         when (length pkgs > 1 || length branches > 1) $ do
           pkg <- getPackageName "."
           if length branches > 1
             then putPkgBrnchHdr pkg br
             else putPkgHdr pkg
         commits <- getLogCommits allbrs simplydecor br
         checkBranchOrder br $ NE.tail commits
         mapM_ (putLogCommit colored) commits
     where
       simplydecor = not nosimplydecor

       checkBranchOrder br oldcommits = do
         let brs = map logBranches oldcommits
             newer = mapMaybe (find ((br<) . toBranch)) brs
         unless (null newer) $
           putStrLn $ showBranch br +-+ "is ahead of:" +-+ unwords (map showBR newer)

       latestBranches :: [Branch] -> IO ()
       latestBranches branches = do
         logbrs <- fmap (NE.toList . NE.nubBy ((==) `on` NE.head)) <$> mapM (getLogCommits allbrs True) $ NE.fromList branches
         let containedIn l = any ((NE.head l `elem`) . NE.tail)
             reduced = [ l | l <- logbrs, not (l `containedIn` logbrs)]
         forM_ reduced $ \r -> do
           checkBranchOrders r
           mapM_ (putLogCommit colored) r
           putChar '\n'

       checkBranchOrders commits = do
         let brs = NE.map logBranches commits
         forM_ (tails1 brs) $ \(b:|bs) ->
           forM_ b $ \b' -> do
           let newer = mapMaybe (find (b'<)) bs
           unless (null newer || allbrs) $
             putStrLn $ showBR b' +-+ "is ahead of:" +-+ unwords (map showBR newer)

data BranchRemote = BR (Maybe String) Branch
  deriving Eq

toBranch :: BranchRemote -> Branch
toBranch (BR _ b) = b

instance Ord BranchRemote where
  compare (BR mr1 b1) (BR mr2 b2) =
    compare (b1,mr1) (b2,mr2)

type BranchList = NonEmpty BranchRemote

data LogCommit = LogCommit
                 { logRef :: String,
                   logBranches :: BranchList,
                   _logText :: String,
                   _logDate :: String }

instance Eq LogCommit where
  lc1 == lc2 = logRef lc1 == logRef lc2

instance Ord LogCommit where
  lc1 <= lc2 = NE.head (logBranches lc1) <= NE.head (logBranches lc2)

logCommitFormat :: String
logCommitFormat = "--pretty=format:%h\US%D\US%s\US%ch"

getLogCommits :: Bool -> Bool -> Branch -> IO (NonEmpty LogCommit)
getLogCommits allbrs simplydecor br = do
  existing <- (if allbrs then listAllBranches else listBranches) True
  unless (br `elem` existing) $
    error' $ "no local branch:" +-+ showBranch br
  ls <- mapMaybe (readLogCommit existing) <$> gitLines "log" (["--simplify-by-decoration" | simplydecor] ++ [logCommitFormat, showBranch br])
  case NE.nonEmpty ls of
    Nothing -> error' $ "empty branch:" +-+ showBranch br
    Just ne -> return ne
  where
    readLogCommit :: [Branch] -> String -> Maybe LogCommit
    readLogCommit existing cs =
      case splitOn "\US" cs of
        [h,ds,s,t] ->
          let mbrs =
                NE.nonEmpty $
                filter (\b' -> any (hasBranch b') existing) $
                mapMaybe (readBranchRemote . dropPrefix "HEAD -> ") $
                splitOn ", " ds
          in
            case mbrs of
              Nothing -> Nothing
              Just brs -> Just $ LogCommit h (NE.sort brs) s t
        _ -> error' $ "malformed log line:" +-+ cs

hasBranch :: BranchRemote -> Branch -> Bool
hasBranch (BR _ b1) b2 = b1 == b2

readBranchRemote :: String -> Maybe BranchRemote
readBranchRemote s =
  case stripInfix "/" s of
    Nothing ->
      case readBranch s of
        Nothing -> Nothing
        Just br -> Just $ BR Nothing br
    Just (r,b) ->
      case readBranch b of
        Nothing -> Nothing
        Just br -> Just $ BR (Just r) br

showBR :: BranchRemote -> String
showBR (BR mr b) =
  case mr of
    Nothing -> showBranch b
    Just r -> r ++ '/' : showBranch b

showLogCommit :: Bool -> LogCommit -> String
showLogCommit colored (LogCommit h brs s time) =
  h +-+ showBranches colored brs +-+ s +-+ '(' : time ++ ")"

putLogCommit :: Bool -> LogCommit -> IO ()
putLogCommit colored = putStrLn . showLogCommit colored

showBranches :: Bool -> BranchList -> String
showBranches colored =
  unwords . reverse . NE.toList . NE.map renderBranches . NE.groupWith1 toBranch
  where
    -- groupSortWith1 :: Ord b => (a -> b) -> NonEmpty a -> NonEmpty (NonEmpty a)
    -- groupSortWith1 f = map (map snd) . NE.groupAllWith1 fst . NE.sortWith fst . map (f &&& id)

    remote (BR mr _) = mr

    renderBranches bs@(b :| t) =
          case uncons t of
            Nothing -> docolor Red $ showBR b
            Just (b',t') ->
              if null t'
              then
                case (remote b, remote b') of
                  (Nothing, Just "origin") ->
                    origin ++ docolor Green (showBranch $ toBranch b)
                  _ -> unwords $ map (docolor Red . showBR) $ NE.toList bs
              else unwords $ map (docolor Magenta . showBR) $ NE.toList bs
      where
        origin = if colored then "origin/" else "{origin/}"

        docolor c = if colored then color c else id


tails1 :: NonEmpty a -> NonEmpty (NonEmpty a)
tails1 =
#if MIN_VERSION_base(4,18,0)
  NE.tails1
#else
  -- from base-4.20
  NE.fromList . Prelude.map NE.fromList . List.init . List.tails . Foldable.toList
#endif