github-tools-0.1.2: src/GitHub/Tools/NetworkGraph.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE ViewPatterns #-}
module GitHub.Tools.NetworkGraph
( getNetworkGraph
) where
import Control.Arrow ((&&&))
import Control.Monad (unless, void)
import Data.Char (ord)
import qualified Data.List as List
import qualified Data.List.Split as List
--import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Vector as V
import qualified GitHub as GH
import qualified GitHub.Data.Name as GH
import Network.HTTP.Client (Manager, newManager)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import System.Directory (doesDirectoryExist,
setCurrentDirectory)
import System.IO (hPutStrLn, stderr)
import System.Process (callProcess, readProcess)
import qualified Text.ParserCombinators.ReadP as R
import GitHub.Tools.Requests
type RepoRef = (GH.Name GH.Owner, GH.Name GH.Repo)
data Ref = Ref String String [String] [String]
deriving Show
instance Read Ref where
readsPrec _ = R.readP_to_S $ Ref
<$> R.count 25 R.get
<*> (R.char ' ' >> readRef)
<*> R.many1 (R.char ' ' >> readRef)
<*> readNames
where
readRef = R.count 40 . R.choice . map R.char $ ['0'..'9'] ++ ['a'..'f']
readNames = do
void $ R.string " ("
res <- R.sepBy (R.munch (not . (`elem` [',',')']))) (R.string ", ")
void $ R.char ')'
return res
toDot :: Ref -> [String]
toDot (Ref _ _ _ []) = []
toDot (Ref date (take 6 -> ref) (map (take 6) -> parents) names@(mainName:_)) =
" \"" <> ref <> "\" [ " <> List.intercalate " " attrs <> " ]"
: edges
where
edges = map (\parent -> " \"" <> ref <> "\" -> \"" <> parent <> "\"") parents
truncateName name
| length name > 25 = take 22 name <> "..."
| otherwise = name
attrs =
[ "label = \"" <> List.intercalate "\\n" (take 10 date : map truncateName names) <> "\""
, "tooltip = \"" <> List.intercalate "\\n" names <> "\""
, "fillcolor = \"" <> nameColor <> "\""
]
palette =
[ "#ccff00"
, "#ccffff"
, "#ffff66"
, "#cccc00"
, "#ccccff"
, "#ffccff"
, "#ffcccc"
, "#ffcc33"
, "#cc9933"
, "#cc9999"
, "#cc99ff"
, "#ff99ff"
, "#ff9933"
, "#cc6633"
, "#66cc99"
, "#99cc33"
, "#009900"
]
nameColor =
case fst $ List.break (`elem` [':','/']) mainName of
"HEAD -> master" -> "red"
"tag" -> "#cccccc"
author -> palette !! ((sum . map ord $ author) `mod` (length palette))
minDate :: String
minDate = "2022-01"
urlBase :: String
urlBase = "https://username:password@github.com/"
denyList :: [GH.Name GH.Owner]
denyList =
[ "4a256b6b3e7t3e8b7t9q7t"
, "activistWannabe2"
, "cha63506"
, "chai3819"
, "CNXTEoEorg"
, "DannaScully"
, "din982"
, "fireeyeusa"
, "grubern"
, "haiiev"
, "innnzzz6"
, "jamiepg1"
, "josephyzhou"
, "jrtorres42"
, "kigu"
, "lucasborer1"
, "lukw00"
, "makianditro1"
, "mehulsbhatt"
, "mk21"
, "nfkd"
, "noikiy"
, "ProgrammerAndHacker"
, "receptpr9001"
, "shaunstanislaus"
, "sometwo"
, "SunelContus"
, "treejames"
, "xeon2007"
, "xuecai"
]
-- | Monadic version of @unless@, taking the condition in the monad
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM condM acc = do
cond <- condM
unless cond acc
getNetworkGraph
:: Maybe GH.Auth
-> [RepoRef]
-> IO String
getNetworkGraph _ [] = return []
getNetworkGraph auth repos@(rootRepo:seedRepos) = do
-- Initialise HTTP manager so we can benefit from keep-alive connections.
mgr <- newManager tlsManagerSettings
unlessM (doesDirectoryExist clonePath) $ do
hPutStrLn stderr "Cloning initial repo"
clone rootRepo
setCurrentDirectory clonePath
hPutStrLn stderr $ "Adding remotes for " <> show (length seedRepos) <> " seed repos"
mapM_ addRemote seedRepos
hPutStrLn stderr $ "Querying GitHub for forks..."
forks <- V.filter (not . (`elem` denyList) . fst) . V.concat <$> mapM (forksFor mgr) repos
hPutStrLn stderr $ "Adding remotes for " <> show (V.length forks) <> " forks"
V.mapM_ addRemote forks
hPutStrLn stderr "Fetching all remotes"
fetchAll
setCurrentDirectory clonePath
dotLines <- concatMap toDot <$> gitLog
return . unlines . concat $
[ [ "strict digraph \"" <> repoPath rootRepo <> "\" {"
, " graph [splines=ortho, rankdir=LR]"
, " node [shape=box width=2.5 margin=\"0,0.02\" style=filled]"
, " edge [dir=back]"
]
, dotLines
, ["}"]
]
where
repoPath (GH.N (Text.unpack -> owner), GH.N (Text.unpack -> repo)) =
owner <> "/" <> repo
clonePath = "/tmp/hub-graph/" <> (Text.unpack . GH.untagName . snd $ rootRepo)
clone repo =
callProcess "git" ["clone", urlBase <> repoPath repo, clonePath]
addRemote repo@(GH.N (Text.unpack -> owner), _) =
callProcess "git" ["remote", "add", owner, urlBase <> repoPath repo]
fetchAll =
callProcess "git" ["fetch", "--all", "--prune", "--jobs=10"]
gitLog =
map read
. filter (>= minDate)
. List.splitOn "\n"
<$> readProcess "git" ["log", "--all", "--format=%cI %H %P%d", "--topo-order", "--simplify-by-decoration"] ""
forksFor :: Manager -> RepoRef -> IO (V.Vector RepoRef)
forksFor mgr (owner, repo) =
V.map (GH.simpleOwnerLogin . GH.repoOwner &&& GH.repoName) <$> request auth mgr (GH.forksForR owner repo GH.FetchAll)