packages feed

pkgtreediff-0.4.1: src/Distribution/RPM/PackageTreeDiff.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

-- | A library for pkgtreediff for comparing trees of rpm packages
module Distribution.RPM.PackageTreeDiff
  (RpmPackage(..),
   readRpmPkg,
   showRpmPkg,
   rpmPkgIdent,
   appendArch,
   dropRpmArch,
   rpmPkgVerRel,
   RpmPackageDiff(..),
   diffPkgs,
   diffPkg,
   Ignore(..),
   Mode(..),
  ) where

#if !MIN_VERSION_base(4,8,0)
import Control.Applicative ((<$>))
#endif
import Data.Char
import Data.Maybe
#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup ((<>))
#endif
import Data.Text (Text)
import qualified Data.Text as T

-- | Mode describes the kind of summary generated by compareDirs
data Mode = AutoSummary | NoSummary | ShowSummary | Added | Deleted | Updated | RST
  deriving Eq

-- | Ignore describes how comparison is done
data Ignore = IgnoreNone    -- ^ do not ignore version or release
            | IgnoreRelease -- ^ ignore differences in release
            | IgnoreVersion -- ^ ignore differences in version
  deriving Eq

type Name = Text
type Arch = Text

data VersionRelease = VerRel Text Text
  deriving Eq

instance Ord VersionRelease where
  compare (VerRel v1 r1) (VerRel v2 r2) =
    case rpmVerCompare v1 v2 of
      EQ -> rpmVerCompare r1 r2
      o -> o

data VerChunk = TildeChunk Int | CaretChunk Int | TxtChunk Text | IntChunk Int
  deriving (Eq,Ord,Show)

data RpmCharCategory = RpmTilde | RpmCaret | RpmOther | RpmLatin | RpmDigit
  deriving Eq

verChunk :: Text -> VerChunk
verChunk t | T.all isDigit t = (IntChunk . read . T.unpack) t
verChunk t | T.all (== '~') t = (TildeChunk . T.length) t
verChunk t | T.all (== '^') t = (CaretChunk . T.length) t
verChunk t = TxtChunk t

rpmVerCompare :: Text -> Text -> Ordering
rpmVerCompare v1 v2 | v1 == v2 = EQ
rpmVerCompare v1 v2 =
  compareChunks (verList v1) (verList v2)
  where
    compareChunks [] [] = EQ
    compareChunks (c:cs) (c':cs') | c == c' = compareChunks cs cs'
    compareChunks ((TildeChunk n):_) ((TildeChunk n'):_) = compare n' n
    compareChunks ((CaretChunk n):_) ((CaretChunk n'):_) = compare n' n
    compareChunks ((TildeChunk _):_) _ = LT
    compareChunks _ ((TildeChunk _):_) = GT
    compareChunks ((CaretChunk _):_) _ = LT
    compareChunks _ ((CaretChunk _):_) = GT
    compareChunks _ [] = GT
    compareChunks [] _ = LT
    compareChunks (c:_) (c':_) = compare c c'

verList :: Text -> [VerChunk]
verList = map verChunk . filter (T.all (/= '.')) . T.groupBy (\ c1 c2 -> rpmCategory c1 == rpmCategory c2)

latinChars :: [Char]
latinChars = ['A'..'Z'] ++ ['a'..'z']

rpmCategory :: Char -> RpmCharCategory
rpmCategory c | isDigit c = RpmDigit
rpmCategory c | c `elem` latinChars = RpmLatin
rpmCategory '~' = RpmTilde
rpmCategory '^' = RpmCaret
rpmCategory _ = RpmOther

-- eqVR True ignore release
eqVR :: Ignore -> VersionRelease -> VersionRelease -> Bool
eqVR IgnoreNone vr vr' = vr == vr'
eqVR IgnoreRelease (VerRel v _) (VerRel v' _) = v == v'
eqVR IgnoreVersion _ _ = True

-- | Text for the version-release of an RpmPackage
rpmPkgVerRel :: RpmPackage -> Text
rpmPkgVerRel = txtVerRel . rpmVerRel
  where
    txtVerRel (VerRel v r) = v <> T.singleton '-' <> r

-- | RPM package with name, version-release, and maybe architecture
data RpmPackage = RpmPkg {rpmName :: Name, rpmVerRel :: VersionRelease, rpmMArch :: Maybe Arch}
  deriving (Eq, Ord)

-- | Text identifier for an RPM package identified by name and arch
rpmPkgIdent :: RpmPackage -> Text
rpmPkgIdent p  = rpmName p <> appendArch p

-- | Helper to add an arch suffix
appendArch :: RpmPackage -> Text
appendArch p = maybe "" ("." <>) (rpmMArch p)

-- | drop arch from RpmPackage
dropRpmArch :: RpmPackage -> RpmPackage
dropRpmArch (RpmPkg n vr _) = RpmPkg n vr Nothing

-- | Render an RpmPackage
showRpmPkg :: RpmPackage -> Text
showRpmPkg p = rpmPkgIdent p <> T.pack "  " <> rpmPkgVerRel p

-- | Parse an RpmPackage
readRpmPkg :: Text -> RpmPackage
readRpmPkg t =
  if compnts < 3 then error $ "Malformed rpm package name: " ++ T.unpack t
  else RpmPkg name (VerRel ver rel) (Just arch)
  where
    compnts = length pieces
    (nvr',arch) = T.breakOnEnd "." $ fromMaybe t $ T.stripSuffix ".rpm" t
    pieces = reverse $ T.splitOn "-" $ T.dropEnd 1 nvr'
    (rel:ver:emaN) = pieces
    name = T.intercalate "-" $ reverse emaN

-- | RpmPackageDiff type encodes how a particular rpm package differs between trees
data RpmPackageDiff = PkgUpdate RpmPackage RpmPackage
                 | PkgAdd RpmPackage
                 | PkgDel RpmPackage
                 | PkgArch RpmPackage RpmPackage
  deriving Eq

-- | Compare two lists of packages NVRs
diffPkgs :: Ignore -> [RpmPackage] -> [RpmPackage] -> [RpmPackageDiff]
diffPkgs _ [] [] = []
diffPkgs ignore (p:ps) [] = PkgDel p : diffPkgs ignore ps []
diffPkgs ignore [] (p:ps) = PkgAdd p : diffPkgs ignore [] ps
diffPkgs ignore (p1:ps1) (p2:ps2) =
  case compareNames p1 p2 of
    LT -> PkgDel p1 : diffPkgs ignore ps1 (p2:ps2)
    EQ -> let diff = diffPkg ignore p1 p2
              diffs = diffPkgs ignore ps1 ps2
          in if isJust diff then fromJust diff : diffs else diffs
    GT -> PkgAdd p2 : diffPkgs ignore (p1:ps1) ps2

-- | Compare two rpms of a package
diffPkg :: Ignore -> RpmPackage-> RpmPackage-> Maybe RpmPackageDiff
diffPkg ignore p1 p2 | rpmPkgIdent p1 == rpmPkgIdent p2 =
                           if eqVR ignore (rpmVerRel p1) (rpmVerRel p2)
                           then Nothing
                           else Just $ PkgUpdate p1 p2
--diffPkg ignore p1 p2 | rpmName p1 == rpmName p2 =
--                            diffPkg ignore True (RpmPkg n1 v1) (RpmPkg n2 v2)
diffPkg _ p1 p2 | rpmName p1 == rpmName p2 && rpmPkgIdent p1 /= rpmPkgIdent p2 = Just $ PkgArch p1 p2
diffPkg _ _ _ = Nothing

compareNames :: RpmPackage -> RpmPackage -> Ordering
compareNames p1 p2 = compare (rpmName p1) (rpmName p2)