module History.CommitInfo ( CommitInfo (..), fromPartialCommitInfos, issueEvents, diffCommitInfos, ) where import Data.Binary (Binary) import Data.Function (on) import Data.List (deleteFirstsBy, find) import Data.Maybe (isJust) import GHC.Generics (Generic) import History.CommitHash (CommitHash) import History.IssueEvent (IssueEvent (..)) import History.PartialCommitInfo (PartialCommitInfo (..)) import Issue (Issue (..)) import Issue.Provenance qualified as I import Prelude -- TODO Change `CommitInfo` -> `CommitIssuesAll` data CommitInfo = CommitInfo { hash :: CommitHash, issues :: [Issue] } deriving (Show, Binary, Generic) fromPartialCommitInfos :: [PartialCommitInfo] -> [CommitInfo] fromPartialCommitInfos [] = [] fromPartialCommitInfos (partialCommitInfo : partialCommitInfos) = scanl propagate (assume partialCommitInfo) partialCommitInfos where assume :: PartialCommitInfo -> CommitInfo assume (PartialCommitInfo {..}) = CommitInfo {..} propagate :: CommitInfo -> PartialCommitInfo -> CommitInfo propagate oldInfo newInfo@(PartialCommitInfo {..}) = CommitInfo { issues = mergeListsBy eq ( \old new -> new { provenance = I.Provenance { first = old.provenance.first, last = if ((/=) `on` (.rawText)) old new then new.provenance.last else old.provenance.last }, closed = False } ) ( \old -> if elemBy eq old newInfo.issues || not (old.file `elem` newInfo.filesChanged) then old else old {closed = True} ) id oldInfo.issues newInfo.issues, .. } eq = (==) `on` (.id) -- | We assume that [CommitInfo] is sorted starting with the oldest -- commits. issueEvents :: [CommitInfo] -> [(CommitHash, [IssueEvent])] issueEvents xs = zip (map (.hash) xs) (zipWith diffCommitInfos predecessors xs) where predecessors = Nothing : map Just xs diffCommitInfos :: Maybe CommitInfo -> CommitInfo -> [IssueEvent] diffCommitInfos maybeOldInfo newInfo = concat [ [IssueCreated newHash issue | issue <- deleteFirstsBy eq newIssues oldIssues], [ IssueChanged newHash (last issues) | issues <- intersectBy' eq newIssues oldIssues, not (null [(x, y) | x <- issues, y <- issues, ((/=) `on` (.rawText)) x y]) ], [IssueDeleted newHash issue | issue <- deleteFirstsBy eq oldIssues newIssues] ] where newHash = newInfo.hash newIssues = newInfo.issues oldIssues = case maybeOldInfo of Nothing -> [] Just oldInfo -> oldInfo.issues eq = (==) `on` id mergeListsBy :: (a -> a -> Bool) -> (a -> a -> b) -> (a -> b) -> (a -> b) -> [a] -> [a] -> [b] mergeListsBy eq onBoth onLeft onRight lefts rights = concat [ [ maybe (onLeft left) (onBoth left) right | left <- lefts, right <- let rights' = filter (eq left) rights in if null rights' then [Nothing] else (map Just rights') ], [ onRight right | right <- rights, not (elemBy eq right lefts) ] ] -- | A variant of `Data.List.intersectBy` that retuns the witnesses of the -- intersection. intersectBy' :: (a -> a -> Bool) -> [a] -> [a] -> [[a]] intersectBy' eq xs ys = filter (not . null) (map (\x -> x : filter (eq x) ys) xs) -- | A variant of `elem` that uses a custom comparison function. elemBy :: (a -> a -> Bool) -> a -> [a] -> Bool elemBy eq x xs = isJust $ find (eq x) xs