module Issue ( Issue (..), Provenance (..), fromComment, id, internalTags, getIssues, ) where import Control.Arrow qualified as W import Control.Exception (handle) import Data.Binary (Binary) import Data.List (find) import Data.Maybe (catMaybes) import Data.Text qualified as T import Data.Time.Clock (UTCTime (utctDay)) import Exception qualified as E import GHC.Generics (Generic) import GHC.Records (HasField (..)) import Issue.Provenance (Author (..), Commit (..), Provenance (..), commitFromHEAD) import Issue.Tag (Tag (..)) import Issue.Tag qualified as I import Issue.Text qualified as I import TreeGrepper.Comment (Comment (..)) import TreeGrepper.Comment qualified as G import TreeGrepper.Match qualified as G import Prelude hiding (id) data Issue = Issue { title :: T.Text, description :: Maybe T.Text, file :: String, provenance :: Provenance, start :: G.Position, end :: G.Position, tags :: [Tag], markers :: [T.Text], rawText :: T.Text } deriving (Show, Binary, Generic, Eq) id :: Issue -> Maybe String id issue = (\(Tag _ v) -> T.unpack <$> v) =<< find (\(Tag k _) -> k == "id") (issue.tags ++ issue.internalTags) internalTags :: Issue -> [Tag] internalTags (Issue {..}) = concat [ [ Tag "id" $ Just $ toSpinalCase title, Tag "title" $ Just $ title, Tag "createdAt" $ Just $ T.pack $ show $ utctDay provenance.first.date, Tag "modifiedAt" $ Just $ T.pack $ show $ utctDay provenance.last.date, Tag "author" $ Just $ provenance.first.author.name, Tag "editor" $ Just $ provenance.last.author.name ], map (Tag "type" . Just) markers ] instance HasField "internalTags" Issue [Tag] where getField issue = internalTags issue toSpinalCase :: T.Text -> T.Text toSpinalCase = T.replace " " "-" . T.filter keep . T.toLower where keep = (`elem` (concat [[' ', '-'], ['a' .. 'z'], ['0' .. '9']])) -- TODO Refactor non-issues -- -- This does not return an issue, as provenance is not computed over its -- history. Maybe this should return a different type, or be internal to -- `History`? fromComment :: FilePath -> Comment -> IO (Maybe Issue) fromComment cwd comment = do commit <- commitFromHEAD cwd let provenance = Provenance commit commit pure ( if any (\marker -> T.isPrefixOf marker title') issueMarkers then Just Issue { title = title, description = description, file = comment.file, provenance = provenance, start = comment.start, end = comment.end, tags = maybe [] I.extractTags description, markers = markers, rawText = rawText } else Nothing ) where rawText = comment.text (title', description) = I.extractText comment.file_type rawText (markers, title) = stripIssueMarkers title' issueMarkers :: [T.Text] issueMarkers = [ "TODO", "FIXME", "QUESTION" ] stripIssueMarkers :: T.Text -> ([T.Text], T.Text) stripIssueMarkers text = case [marker | marker <- issueMarkers, T.isPrefixOf marker text] of (marker : _) -> W.first (marker :) . stripIssueMarkers $ T.stripStart (T.drop (T.length marker) text) [] -> ([], text) -- | Get all issues in the given directory and file. getIssues :: FilePath -> FilePath -> IO [Issue] getIssues cwd filename = handle (\(_ :: E.UnknownFileExtension) -> pure []) $ fmap catMaybes . mapM (fromComment cwd) =<< G.getComments cwd filename