github-tools-0.1.2: src/GitHub/Tools/Changelogs.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
module GitHub.Tools.Changelogs
( fetchChangeLog
, formatChangeLog
, ChangeLog
) where
import Control.Arrow (first, second, (&&&))
import qualified Control.Monad.Parallel as P
import Data.List (foldl')
import qualified Data.List as List
import qualified Data.List.Split as List
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Vector as V
import Debug.Trace (traceIO)
import qualified GitHub
import Network.HTTP.Client (newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Text.Read (readMaybe)
-- import Text.Groom (groom)
import GitHub.Tools.Requests
newtype ChangeLog = ChangeLog { unChangeLog :: [(Text, [Text], [Text])] }
data VersionComponent
= Number Int
| Wildcard
deriving (Ord, Eq)
instance Read VersionComponent where
readsPrec _ ('x':s) = [(Wildcard, s)]
readsPrec p input = map (first Number) . readsPrec p $ input
data Version
= VersionNumber [VersionComponent]
| VersionString Text
deriving (Ord, Eq)
formatChangeLog :: Bool -> ChangeLog -> Text
formatChangeLog wantRoadmap =
(<> "\n") . foldl' (<>) "" . map formatMilestone . unChangeLog
where
prsWord = if wantRoadmap then "PRs to review" else "Merged PRs"
issuesWord = if wantRoadmap then "Planned tasks" else "Closed issues"
formatMilestone (milestone, issues, pulls) =
"\n\n## " <> milestone
<> mergedPrs pulls
<> closedIssues issues
mergedPrs [] = ""
mergedPrs pulls =
foldl' (<>) ("\n\n### " <> prsWord <> ":\n") . itemise $ pulls
closedIssues [] = ""
closedIssues issues =
foldl' (<>) ("\n\n### " <> issuesWord <> ":\n") . itemise $ issues
itemise = map ("\n- " <>)
data ChangeLogItemKind
= Issue
| PullRequest
deriving Show
data ChangeLogItem = ChangeLogItem
{ clMilestone :: Text
, clTitle :: Text
, clNumber :: Int
}
groupByMilestone
:: (ChangeLogItem -> ([a], [a]) -> ([a], [a]))
-> [ChangeLogItem]
-> Map.Map Text ([a], [a])
-> Map.Map Text ([a], [a])
groupByMilestone add = flip $ foldr (\item group ->
Map.insert (clMilestone item) (
case Map.lookup (clMilestone item) group of
Just old -> add item old
Nothing -> add item ([], [])
) group)
formatChangeLogItem
:: ChangeLogItemKind
-> GitHub.Name GitHub.Owner
-> GitHub.Name GitHub.Repo
-> ChangeLogItem
-> Text
formatChangeLogItem kind ownerName repoName item =
"[#" <> number <> "](https://github.com/" <> GitHub.untagName ownerName <>
"/" <> GitHub.untagName repoName <> kindPart <> number <> ") " <>
clTitle item
where
number = Text.pack . show . clNumber $ item
kindPart =
case kind of
Issue -> "/issues/"
PullRequest -> "/pull/"
makeChangeLog
:: Bool
-> GitHub.Name GitHub.Owner
-> GitHub.Name GitHub.Repo
-> [GitHub.SimplePullRequest]
-> [GitHub.Issue]
-> ChangeLog
makeChangeLog wantRoadmap ownerName repoName pulls issues =
ChangeLog
. reverseIfChangelog
. sortChangelog
. Map.foldlWithKey (\changes milestone (msIssues, msPulls) ->
if milestone == "meta"
then changes
else
( milestone
, map (formatChangeLogItem Issue ownerName repoName) msIssues
, map (formatChangeLogItem PullRequest ownerName repoName) msPulls
) : changes
) []
. groupByMilestone (first . (:)) changeLogIssues
. groupByMilestone (second . (:)) changeLogPrs
$ Map.empty
where
reverseIfChangelog l =
if wantRoadmap
then l
else reverse l
sortChangelog :: [(Text, [Text], [Text])] -> [(Text, [Text], [Text])]
sortChangelog = List.sortOn $ \(v, _, _) -> parseVersion v
parseVersion :: Text -> Version
parseVersion v =
case Text.unpack v of
-- Milestones starting with "v" must be versions.
'v':version ->
case map readMaybe $ List.splitOn "." version of
[Just major, Just minor, Just rev] -> VersionNumber [major, minor, rev]
_ -> error $ "invalid version: " <> version
_ -> VersionString v
(mergedPrs, openPrs) = List.partition (\case
GitHub.SimplePullRequest
{ GitHub.simplePullRequestMergedAt = Just _ } -> True
_ -> False
) pulls
selectedPrs =
if wantRoadmap
then openPrs
else mergedPrs
selectedItems = filter (\case
GitHub.Issue
{ GitHub.issueMilestone = Just GitHub.Milestone
{ GitHub.milestoneState = state }
} ->
if wantRoadmap
then state == "open"
else state == "closed"
_ -> False
) issues
(backlogPrs, backlogIssues) =
if wantRoadmap
then
-- Split into PRs and non-PR issues.
List.partition (\case
GitHub.Issue { GitHub.issuePullRequest = Just _ } -> True
_ -> False
)
-- Filter by issues that don't have a milestone.
. filter (\case
GitHub.Issue { GitHub.issueMilestone = Nothing } -> True
_ -> False
)
$ issues
else
([], [])
addToBacklog =
map $ \issue -> ChangeLogItem
{ clMilestone = "Backlog"
, clTitle = GitHub.issueTitle issue
, clNumber = GitHub.unIssueNumber $ GitHub.issueNumber issue
}
backlogPrItems = addToBacklog backlogPrs
backlogIssueItems = addToBacklog backlogIssues
milestoneByIssueId =
Map.fromList
. map (GitHub.issueNumber &&& GitHub.milestoneTitle . Maybe.fromJust . GitHub.issueMilestone)
$ selectedItems
changeLogIssues :: [ChangeLogItem]
changeLogIssues = backlogIssueItems ++ Maybe.mapMaybe (\case
-- filter out PRs
GitHub.Issue { GitHub.issuePullRequest = Just _ } -> Nothing
issue -> Just ChangeLogItem
{ clMilestone = GitHub.milestoneTitle . Maybe.fromJust . GitHub.issueMilestone $ issue
, clTitle = GitHub.issueTitle issue
, clNumber = GitHub.unIssueNumber $ GitHub.issueNumber issue
}
) selectedItems
changeLogPrs :: [ChangeLogItem]
changeLogPrs = backlogPrItems ++ Maybe.mapMaybe (\issue -> do
milestone <- flip Map.lookup milestoneByIssueId . GitHub.simplePullRequestNumber $ issue
return ChangeLogItem
{ clMilestone = milestone
, clTitle = GitHub.simplePullRequestTitle issue
, clNumber = GitHub.unIssueNumber $ GitHub.simplePullRequestNumber issue
}
) selectedPrs
fetchChangeLog
:: Bool
-> GitHub.Name GitHub.Owner
-> GitHub.Name GitHub.Repo
-> Maybe GitHub.Auth
-> IO ChangeLog
fetchChangeLog wantRoadmap ownerName repoName auth = do
-- Initialise HTTP manager so we can benefit from keep-alive connections.
mgr <- newManager tlsManagerSettings
let fetchPulls state = do
traceIO $ "Fetching pull requests"
l <- V.toList <$> request auth mgr (GitHub.pullRequestsForR ownerName repoName state GitHub.FetchAll)
traceIO $ "Got " <> show (length l) <> " pull requests"
return l
let fetchIssues state = do
traceIO $ "Fetching issues"
l <- V.toList <$> request auth mgr (GitHub.issuesForRepoR ownerName repoName state GitHub.FetchAll)
traceIO $ "Got " <> show (length l) <> " issues"
return l
-- issues >>= putStrLn . groom
(pulls, issues) <- uncurry (P.bindM2 (\p i -> return (p, i))) $
if wantRoadmap
then (fetchPulls GitHub.stateOpen , fetchIssues GitHub.stateOpen )
else (fetchPulls GitHub.stateClosed, fetchIssues GitHub.stateClosed)
traceIO "Formatting changelog/roadmap"
return $ makeChangeLog wantRoadmap ownerName repoName pulls issues