packages feed

checkmate-0.1.0: app/Main.hs

{-# LANGUAGE OverloadedStrings #-}
import Data.Foldable
import Data.Maybe
import Data.Semigroup ((<>))
import Prelude hiding (error)
import System.Environment
import System.Exit
import System.IO

import Data.Text hiding (find)
import Data.Text.Encoding
import Data.Text.IO as TIO
import GitHub.Auth
import GitHub.Data.Comments
import GitHub.Data.Definitions
import GitHub.Data.Id
import GitHub.Data.Issues
import GitHub.Data.Name
import GitHub.Data.Repos
import GitHub.Data.URL
import GitHub.Endpoints.Issues.Comments (comments', createComment, editComment)
import GitHub.Endpoints.Users (userInfoCurrent')
import Options.Applicative
import System.Directory

import Checkmate.Check
import Checkmate.Discover

type Command = App -> Checklist -> IO ()

data App = App
    { inputFilePath :: FilePath
    , appCommand :: Command
    }

withInputFile :: App -> (Handle -> IO r) -> IO r
withInputFile App { inputFilePath = "-" } action' = do
    let i = stdin
    r <- action' i
    hClose i
    return r
withInputFile App { inputFilePath = i } action' =
    withFile i ReadMode action'

appP :: Parser App
appP = App
    <$> strOption (  long "input-file"
                  <> short 'i'
                  <> metavar "FILE"
                  <> showDefault
                  <> value "-"
                  <> help "A diff text to extract a checklist from"
                  )
    <*> subparser (  command "commonmark" commonmarkPI
                  <> command "gfm" gfmPI
                  <> command "github" githubPI
                  )

commonmarkPI :: ParserInfo Command
commonmarkPI = info (pure cmd) $
    progDesc "Print a checklist as CommonMark (i.e. Markdown) format."
  where
    cmd :: Command
    cmd _ = TIO.putStr . toCommonMark

gfmPI :: ParserInfo Command
gfmPI = info (pure cmd) $
    progDesc "Print a checklist as GitHub Flavored Markdown format."
  where
    cmd :: Command
    cmd _ = TIO.putStr . toGFMarkdown

githubPI :: ParserInfo Command
githubPI = info (parser <**> helper) $
    progDesc $ "Create a checklist comment on the corresponding pull " ++
               "reuqest on GitHub."
  where
    cmd :: Maybe (Name Owner)
        -> Name Repo
        -> Id Issue
        -> Token
        -> Maybe Text
        -> Command
    cmd owner' repo pr accessToken endpoint _ checklist = do
        user <- userInfoCurrent' auth >>= error
        let owner = fromMaybe (N . untagName $ userLogin user) owner'
        prComments <- comments' (Just auth) owner repo pr >>= error
        let checklistComment = find (isChecklist user) prComments
            leave = case checklistComment of
                Nothing -> createComment auth owner repo pr
                Just IssueComment { issueCommentId = cid } ->
                    editComment auth owner repo $ Id cid
        Comment { commentHtmlUrl = leftCommentUrl } <-
            leave (signature `append` toGFMarkdown checklist) >>= error
        case leftCommentUrl of
            Just (URL u) -> TIO.putStrLn u
            _ -> return ()
      where
        auth :: Auth
        auth = case endpoint of
            Nothing -> OAuth accessToken
            Just e -> EnterpriseOAuth e accessToken
        signature :: Text
        signature = "<!-- COMMENT BY CHECKMATE -->\n"
        isChecklist :: User -> IssueComment -> Bool
        isChecklist User { userId = uid }
                    IssueComment { issueCommentBody = cBody
                                 , issueCommentUser =
                                       SimpleUser { simpleUserId = authorId }
                                 } =
            uid == authorId && isPrefixOf signature cBody
    error :: Either Error a -> IO a
    error (Right v) = return v
    error (Left (HTTPError httpError)) = printError $ pack $ show httpError
    error (Left (ParseError message)) = printError message
    error (Left (JsonError message)) = printError message
    error (Left (UserError message)) = printError message
    parser :: Parser Command
    parser = cmd
        <$> option (Just . (N :: Text -> Name Owner) . pack <$> str)
            (  long "owner"
            <> long "login"
            <> short 'l'
            <> metavar "LOGIN"
            <> value Nothing
            <> help ("Owner of GitHub repository of a pull request to " ++
                     "create a checklist comment.  \"foo\" of " ++
                     "\"github.com/foo/bar\".  The currently authenticated " ++
                     "user (through -t/--access-token/--token) by default")
            )
        <*> option ((N :: Text -> Name Repo) . pack <$> str)
            (  long "repository"
            <> long "repo"
            <> short 'r'
            <> metavar "REPO"
            <> help ("Name of GitHub repository of a pull request to create " ++
                     "a checklist comment.  \"bar\" of \"github.com/foo/bar\"")
            )
        <*> option ((Id :: Int -> Id Issue) <$> (auto :: ReadM Int))
            (  long "pull-request"
            <> long "pr"
            <> short 'p'
            <> metavar "NUM"
            <> help "No. of pull request to create a checklist comment"
            )
        <*> option (encodeUtf8 . pack <$> str)
            (  long "access-token"
            <> long "token"
            <> short 't'
            <> metavar "TOKEN"
            <> help "GitHub OAuth access token"
            )
        <*> option (Just . dropWhileEnd (== '/') . pack <$> str)
            (  long "enterprise-endpoint"
            <> short 'e'
            <> metavar "URL"
            <> value Nothing
            <> help "API endpoint URL for GitHub Enterprise (if applicable)"
            )

appPI :: ParserInfo App
appPI = info (appP <**> helper)
    (  fullDesc
    <> progDesc "Generate checklists relevant to a given patch."
    )

toCommonMark' :: Checklist -> Text -> Text
toCommonMark' checklist itemPrefix =
    "### Checklist  \x1f914\n\n" `append` intercalate
        "\n"
        [ " -  " `append` itemPrefix `append` replace "\n" "\n    " t
        | Check { checkText = t } <- toList checklist
        ]

toGFMarkdown :: Checklist -> Text
toGFMarkdown = (`toCommonMark'` "[ ] ")

toCommonMark :: Checklist -> Text
toCommonMark = (`toCommonMark'` "")

printError :: Text -> IO a
printError message = do
    prog <- getProgName
    die $ prog ++ ": error: " ++ unpack message

main :: IO ()
main = do
    app@App { appCommand = cmd' } <- execParser appPI
    cwd <- getCurrentDirectory
    diff <- withInputFile app TIO.hGetContents
    case parseDiff diff of
        Left msg -> System.IO.putStrLn msg
        Right deltas -> do
            checklist <- discover cwd deltas
            cmd' app checklist