209 lines
6.7 KiB
Haskell
209 lines
6.7 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Main where
|
|
|
|
import Control.Monad (forM, forM_)
|
|
import qualified Data.ByteString as B
|
|
import qualified Data.ByteString.Char8 as BC
|
|
import Data.Frontmatter (parseYamlFrontmatterEither)
|
|
import Data.Map (Map)
|
|
import qualified Data.Map as M
|
|
import Data.Maybe (fromMaybe, mapMaybe)
|
|
import Data.String (IsString(..))
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import qualified Data.Vector as V
|
|
import Data.Yaml (FromJSON(..), ToJSON(..), (.:), (.:?), (.=))
|
|
import qualified Data.Yaml as Y
|
|
import qualified GitHub as GH
|
|
import System.Directory (createDirectoryIfMissing)
|
|
import System.Exit (exitFailure)
|
|
import System.FilePath ((</>), (<.>))
|
|
import System.FilePath.Glob (namesMatching)
|
|
import System.Environment (lookupEnv)
|
|
import Text.Printf (printf)
|
|
|
|
|
|
-- * Configuration
|
|
|
|
authorDir, contributorDir :: FilePath
|
|
authorDir = "authors"
|
|
contributorDir = "contributors"
|
|
|
|
githubOwner, githubRepo :: Text
|
|
githubOwner = "plfa"
|
|
githubRepo = "plfa.github.io"
|
|
|
|
githubErrors :: [Text]
|
|
githubErrors = ["invalid-email-address"]
|
|
|
|
|
|
-- * Main
|
|
|
|
main :: IO ()
|
|
main = do
|
|
-- Get the GitHub authentication token from `GITHUB_TOKEN`
|
|
auth <- getAuth
|
|
|
|
-- Read the current set of authors from `authorDir`
|
|
authors <- readAuthors authorDir
|
|
|
|
-- Read the current set of contributors from `contributorDir`
|
|
localContributors <- readContributors contributorDir
|
|
remoteContributorAndAuthors <-
|
|
getContributors auth (GH.mkOwnerName githubOwner) (GH.mkRepoName githubRepo)
|
|
|
|
-- Filter the contributor list by the authors
|
|
let authorGithubs :: [Text]
|
|
authorGithubs = map authorGithub authors
|
|
|
|
let isAuthorOrError :: Contributor -> Bool
|
|
isAuthorOrError Contributor{..} =
|
|
contributorGithub `elem` authorGithubs || contributorGithub `elem` githubErrors
|
|
|
|
let remoteContributors :: [Contributor]
|
|
remoteContributors = filter (not . isAuthorOrError) remoteContributorAndAuthors
|
|
|
|
-- Turn contributor lists into maps and merge them
|
|
let localContributorMap :: Map Text Contributor
|
|
localContributorMap = M.fromList [(contributorGithub c, c) | c <- localContributors]
|
|
|
|
let remoteContributorMap :: Map Text Contributor
|
|
remoteContributorMap = M.fromList [(contributorGithub c, c) | c <- remoteContributors]
|
|
|
|
let contributorMap :: Map Text Contributor
|
|
contributorMap = M.unionWith (<>) localContributorMap remoteContributorMap
|
|
|
|
-- Write contributor files
|
|
createDirectoryIfMissing True contributorDir
|
|
forM_ (M.toList contributorMap) $ \(github, contributor) -> do
|
|
let contributorFile = contributorDir </> T.unpack github <.> "metadata"
|
|
let contributorBS = "---\n" <> Y.encode contributor <> "---\n"
|
|
BC.writeFile contributorFile contributorBS
|
|
|
|
|
|
-- * Authors
|
|
|
|
data Author = Author
|
|
{ authorName :: Text
|
|
, authorEmail :: Text
|
|
, authorCorresponding :: Bool
|
|
, authorGithub :: Text
|
|
, authorTwitter :: Maybe Text
|
|
}
|
|
deriving (Show)
|
|
|
|
readAuthors :: FilePath -> IO [Author]
|
|
readAuthors dir = do
|
|
authorFiles <- namesMatching (dir </> "*.metadata")
|
|
forM authorFiles $ \authorFile -> do
|
|
authorOrError <- parseYamlFrontmatterEither <$> B.readFile authorFile
|
|
case authorOrError of
|
|
Left errmsg -> do
|
|
printf "Parse error in '%s': %s\n" authorFile errmsg
|
|
exitFailure
|
|
Right author ->
|
|
return author
|
|
|
|
instance FromJSON Author where
|
|
parseJSON = Y.withObject "Author" $ \v -> Author
|
|
<$> v .: "name"
|
|
<*> v .: "email"
|
|
<*> v .: "corresponding"
|
|
<*> v .: "github"
|
|
<*> v .:? "twitter"
|
|
|
|
instance ToJSON Author where
|
|
toJSON Author{..} =
|
|
Y.object [ "name" .= authorName
|
|
, "email" .= authorEmail
|
|
, "corresponding" .= authorCorresponding
|
|
, "github" .= authorGithub
|
|
, "twitter" .= authorTwitter
|
|
]
|
|
|
|
|
|
-- * Contributors
|
|
|
|
data Contributor = Contributor
|
|
{ contributorName :: Text
|
|
, contributorGithub :: Text
|
|
, contributorCount :: Int
|
|
}
|
|
|
|
instance Semigroup Contributor where
|
|
Contributor _name1 github1 count1 <> Contributor name2 github2 count2
|
|
= if github1 == github2
|
|
then Contributor name2 github2 (count1 `max` count2)
|
|
else error $ printf "Cannot merge unrelated contributors '%s' and '%s'" github1 github2
|
|
|
|
instance FromJSON Contributor where
|
|
parseJSON = Y.withObject "Contributor" $ \v -> Contributor
|
|
<$> v .: "name"
|
|
<*> v .: "github"
|
|
<*> v .: "count"
|
|
|
|
instance ToJSON Contributor where
|
|
toJSON Contributor{..} =
|
|
Y.object [ "name" .= contributorName
|
|
, "github" .= contributorGithub
|
|
, "count" .= contributorCount
|
|
]
|
|
|
|
readContributors :: FilePath -> IO [Contributor]
|
|
readContributors dir = do
|
|
contributorFiles <- namesMatching (dir </> "*.metadata")
|
|
forM contributorFiles $ \contributorFile -> do
|
|
contributorOrError <- parseYamlFrontmatterEither <$> B.readFile contributorFile
|
|
case contributorOrError of
|
|
Left errmsg -> do
|
|
printf "Parse error in '%s': %s\n" contributorFile errmsg
|
|
exitFailure
|
|
Right contributor ->
|
|
return contributor
|
|
|
|
|
|
-- * Github interaction
|
|
|
|
-- |Get user information for every user who authored a commit.
|
|
getContributors :: GH.Auth -> GH.Name GH.Owner -> GH.Name GH.Repo -> IO [Contributor]
|
|
getContributors auth owner repo = do
|
|
commits <- getCommits auth owner repo
|
|
forM (frequency (mapMaybe GH.commitAuthor commits)) $ \(simpleUser, count) -> do
|
|
user <- getUserInfo auth simpleUser
|
|
return $ toContributor user count
|
|
|
|
-- |Convert a |GH.User| value to a |Contributor| value.
|
|
toContributor :: GH.User -> Int -> Contributor
|
|
toContributor commitAuthor count = Contributor name github count
|
|
where
|
|
name = fromMaybe github (GH.userName commitAuthor)
|
|
github = GH.untagName (GH.userLogin commitAuthor)
|
|
|
|
-- |Get an authentication token from the environment.
|
|
getAuth :: IO GH.Auth
|
|
getAuth = do
|
|
mtoken <- lookupEnv "GITHUB_TOKEN"
|
|
case mtoken of
|
|
Nothing -> error "Please set GITHUB_TOKEN"
|
|
Just token -> return (GH.OAuth . fromString $ token)
|
|
|
|
-- |Get user information from a user login.
|
|
getUserInfo :: GH.Auth -> GH.SimpleUser -> IO GH.User
|
|
getUserInfo auth simpleUser =
|
|
fromRight =<< GH.github auth GH.userInfoForR (GH.simpleUserLogin simpleUser)
|
|
|
|
-- |Get commit history for a repository.
|
|
getCommits :: GH.Auth -> GH.Name GH.Owner -> GH.Name GH.Repo -> IO [GH.Commit]
|
|
getCommits auth owner repo =
|
|
V.toList <$> (fromRight =<< GH.github auth GH.commitsForR owner repo GH.FetchAll)
|
|
|
|
|
|
-- * Utils
|
|
|
|
frequency :: (Ord a) => [a] -> [(a, Int)]
|
|
frequency xs = M.toList (M.fromListWith (+) [(x, 1) | x <- xs])
|
|
|
|
fromRight :: Show e => Either e a -> IO a
|
|
fromRight = either (fail . show) return
|