module History.PartialCommitInfo ( PartialCommitInfo (..), getPartialCommitInfos, ) where import Control.Exception (catch, handle) import Data.Binary (Binary) import Data.ByteString.Lazy.Char8 qualified as LB8 import Data.Function ((&)) import Data.List.NonEmpty qualified as N import Data.Maybe (catMaybes) import Data.Text qualified as T import Die (die) import Exception qualified as E import GHC.Generics (Generic) import Git qualified import History.Cache (cached) import History.CommitHash (CommitHash (..)) import Issue (Issue (..)) import Issue.Provenance qualified as I import Issue.Tag qualified as I import Issue.Text qualified as I import Parallel (parMapM) import Process (proc, sh) import System.Directory (getCurrentDirectory) import System.FilePath (()) import System.IO.Temp (withSystemTempDirectory) import System.Process.Typed (setWorkingDir) import TreeGrepper.Comment qualified as G -- | `PartialCommitInfo` records the complete issues ONLY in files that have -- been changed in the commit. -- TODO Change `PartialCommitInfo` -> `CommitIssuesChanged` data PartialCommitInfo = PartialCommitInfo { hash :: CommitHash, filesChanged :: [FilePath], issues :: [Issue] } deriving (Show, Binary, Generic) getPartialCommitInfos :: IO [PartialCommitInfo] getPartialCommitInfos = do commitHashes <- N.toList <$> Git.getCommitHashes parMapM getCommitInfoOf (map Commit commitHashes ++ [WorkingTree]) getCommitInfoOf :: CommitHash -> IO PartialCommitInfo getCommitInfoOf WorkingTree = do (issuesWorkingTreeChanged, filesChanged) <- getIssuesAndFilesWorkingTreeChanged [] pure $ PartialCommitInfo { hash = WorkingTree, filesChanged = filesChanged, issues = issuesWorkingTreeChanged } getCommitInfoOf (Commit hash) = cached (hash <> (T.pack ".changed")) $ \_ -> do (issuesCommitChanged, filesChanged) <- getIssuesAndFilesCommitChanged hash pure $ PartialCommitInfo { hash = Commit hash, filesChanged = filesChanged, issues = issuesCommitChanged } -- | Given the hash of a commit, get all issues in the files which have -- been changed by this commit, as well as all changed files. getIssuesAndFilesCommitChanged :: T.Text -> IO ([Issue], [FilePath]) getIssuesAndFilesCommitChanged hash = do withSystemTempDirectory "history" $ \tmp -> do let cwd = tmp T.unpack hash Git.withWorkingTree cwd hash do files <- gitShowChanged cwd issues <- concat <$> catch (mapM (getIssues cwd) files) dieOfInvalidTreeGrepperResult pure (issues, files) -- | 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 -- | Note that `provenance` is trivial and needs to be fixed up later. fromComment :: FilePath -> G.Comment -> IO (Maybe Issue) fromComment cwd comment = do commit <- I.commitFromHEAD cwd let provenance = I.Provenance commit commit pure ( if any (\marker -> T.isPrefixOf marker title') I.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, commentStyle = commentStyle } else Nothing ) where (commentStyle, rawText) = G.uncomment comment.file_type comment.text (title', description) = I.extractText rawText (markers, title) = I.stripIssueMarkers title' dieOfInvalidTreeGrepperResult :: E.InvalidTreeGrepperResult -> IO a dieOfInvalidTreeGrepperResult (E.InvalidTreeGrepperResult e) = die e -- | Gets issues in all files which have been changed in your current -- [working -- - tree](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefworkingtreeaworkingtree) getIssuesAndFilesWorkingTreeChanged :: [FilePath] -> IO ([Issue], [FilePath]) getIssuesAndFilesWorkingTreeChanged paths = do cwd <- getCurrentDirectory files <- gitLsFilesModifiedIn cwd paths issues <- concat <$> catch (mapM (getIssues cwd) files) dieOfInvalidTreeGrepperResult pure (issues, files) gitShowChanged :: FilePath -> IO [FilePath] gitShowChanged cwd = Prelude.lines . LB8.unpack <$> sh ("git show -p --name-only --format=''" & setWorkingDir cwd) gitLsFilesModifiedIn :: FilePath -> [FilePath] -> IO [FilePath] gitLsFilesModifiedIn cwd paths = Prelude.lines . LB8.unpack <$> sh ( proc "git ls-files --modified %" ("--" : paths) & setWorkingDir cwd )