darcswatch-0.3: src/Darcs/Watch/GenerateOutput.hs
{-
Copyright (C) 2008 Joachim Breitner
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
-}
module Darcs.Watch.GenerateOutput where
import Control.Monad
import Control.Concurrent
import Control.Applicative
import System.Environment (getArgs)
import System.Directory
import System.Posix.Files
import System.Time
import System.IO
import System.Terminal.Concurrent
import qualified Data.Map as M
import qualified Data.Set as S
import qualified MultiMap as MM
import MultiMap ((!!!!))
import Data.Char
import Data.List
import Data.Time
import System.FilePath
import qualified Data.ByteString.Char8 as B
import Data.ByteString.Char8 (ByteString)
-- Darcs stuff
import Darcs
import Darcs.Watch.Storage
import Darcs.Watch.Data
-- Web ouput
import HTML
import Data.Digest.OpenSSL.MD5 (md5sum)
import Data.Maybe
generateOutput config patchNew = do
nowStamp <- getCurrentTime
let outputStampFile = cData config </> "output.stamp"
ex <- doesFileExist outputStampFile
lastStamp <- if ex then read . B.unpack <$> B.readFile outputStampFile
else return $ UTCTime (ModifiedJulianDay 0) 0
putStrLn "Reading repositories..."
let loadInv rep = do
putStr $ "Reading " ++ rep ++ " ...\n"
ps <- readRepository (cData config) rep
repoInfo <- getRepositoryInfo (cData config) rep
let thisNew = maybe True (>= lastStamp) (lastUpdate repoInfo)
putStr (if thisNew then "Repostory is new.\n" else "Repository is cached.\n")
return (rep, ps, repoInfo, thisNew)
readInv (p2r,r2p,r2ri,new) (rep, ps, repoInfo, thisNew) = do
let p2r' = foldr (\p -> MM.append p rep) p2r ps
r2p' = MM.extend rep ps r2p :: M.Map RepositoryURL (S.Set PatchInfo)
r2ri' = M.insert rep repoInfo r2ri
return (p2r', r2p', r2ri', new || thisNew)
(p2r,r2p, r2ri, new) <- foldM readInv (MM.empty, MM.empty, M.empty, patchNew) =<<
sequence (map loadInv (cRepositories config))
putStrLn "Reading emails..."
bundleHashes <- listBundles (cData config)
let sortInBundle (u2p, u2rn, p2pe, new) bundleHash = do
putStrLn $ "Reading mail " ++ bundleHash ++ " ..."
bundle <- getBundle (cData config) bundleHash
let bundleFileName = getBundleFileName (cData config) bundleHash
history <- getBundleHistory (cData config) bundleHash
let thisNew = any (\(d,_,_) -> d >= lastStamp) history
let (patches,context) = bundle
let u2p' = foldr (\(p,_) -> MM.append (normalizeAuthor (piAuthor p)) p) u2p patches
let u2rn' = foldr (\(p,_) ->
M.insertWith (maxBy B.length) (normalizeAuthor (piAuthor p)) (piAuthor p)
) u2rn patches
let p2pe' = foldr (\(p,d) ->
let pe = PatchExtras d context bundleFileName history
-- The patch with the smaller context is the more useful
in M.insertWith (minBy (length.peContext)) p pe
) p2pe patches
return (u2p', u2rn', p2pe', new || thisNew)
(u2p, u2rn, p2pe, new') <- foldM sortInBundle (MM.empty, M.empty, M.empty, new) bundleHashes
let patches = M.keys p2pe -- Submitted patches
let repos = M.keys r2p -- Repos with patches
let users = M.keys u2p -- Known users
if not new' then putStrLn "Nothing new, exiting" else do
-- Clonsider patches as belonging to a repository when either
-- * it is already applied
-- * all its context is in the repository
-- * at least 10 patches of the context are in the repository
let addables = do -- List monad
patch <- patches
repo <- repos
present <- maybeToList $ M.lookup repo r2p
if patch `S.member` present
then return (patch,repo)
else do
pe <- maybeToList $ M.lookup patch p2pe
case partition (`S.member` present) (peContext pe) of
(_,[]) -> return (patch,repo)
(m,_) | length m >= 10 -> return (patch,repo)
_ -> []
-- Patch to possible repos
-- Repo to possible patch
let p2pr = foldr (\(p,r) -> MM.append p r) MM.empty addables
let r2mp = foldr (\(p,r) -> MM.append r p) MM.empty addables
-- Unmatched patches
let unmatched = S.fromList $
-- filter (\p -> M.findWithDefault Unmatched p p2s == Unmatched) $
filter (\p -> not (M.member p p2pr)) patches
now <- getClockTime >>= toCalendarTime
let resultData = ResultData p2r r2p u2p p2pe p2pr r2mp r2ri unmatched now u2rn
{-
putStrLn "Evalutating data"
putStrLn (show (length (show (resultData))))
-}
putStrLn "Writing output..."
writeFile (cOutput config ++ "/index.html") (mainPage resultData)
forM_ users $ \u ->
writeFile (cOutput config ++ "/" ++ userFile u) (userPage resultData u)
forM_ repos $ \r ->
writeFile (cOutput config ++ "/" ++ repoFile r) (repoPage resultData r)
forM_ patches $ \p ->
B.writeFile (cOutput config ++ "/" ++ patchDiffFile p) (peDiff (p2pe M.! p))
writeFile (cOutput config ++ "/" ++ "unmatched.html") (unmatchedPage resultData)
putStrLn "Linking patches"
let patchLink (p,pe) = do
let link = cOutput config ++ "/" ++ patchBasename p ++ ".dpatch"
ex <- fileExist link
unless ex $ do
-- There might be a broken symlink here:
catch (removeFile link) (const (return ()))
createSymbolicLink (peBundleFile pe) link
mapM_ patchLink $ M.toList p2pe
B.writeFile outputStampFile (B.pack (show nowStamp))
getDirectoryFiles dir' = getDirectoryContents dir >>=
return . (map (dir++)) >>=
return . filter ((/= '.') . head) >>=
filterM doesFileExist >>=
filterM ((readable `fmap`) . getPermissions)
where dir = addSlash dir'
maxBy f v1 v2 = if f v1 >= f v2 then v1 else v2
minBy f v1 v2 = if f v1 <= f v2 then v1 else v2
addSlash filename | last filename == '/' = filename
| otherwise = filename ++ "/"
-- not in ghc6.6
infixl 0 `on`
on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
(*) `on` f = \x y -> f x * f y