{- This file is part of hit-graph. - - Written in 2016 by fr33domlover . - - ♡ Copying is an act of love. Please copy, reuse and share. - - The author(s) have dedicated all copyright and related and neighboring - rights to this software to the public domain worldwide. This software is - distributed without any warranty. - - You should have received a copy of the CC0 Public Domain Dedication along - with this software. If not, see - . -} module Data.Git.Graph ( NodeLabel , EdgeLabel , CommitGraph , loadCommitGraph , loadCommitGraphPT , Depth , commitDepths , filterDepth , partitionDepth , isHealthy , isShallow , isExcluded --, loadCommitGraphByNameMaybe --, loadCommitGraphByName --, loadCommitsTopsort --, loadCommitsTopsortList ) where import Control.Monad.IO.Class import Data.Foldable (foldl', foldlM) import Data.Git.Named (RefName (..)) import Data.Git.Ref (Ref, toBinary) import Data.Git.Repository (getCommit, resolveRevision, branchList, tagList) import Data.Git.Revision (Revision (..)) import Data.Git.Storage (Git) import Data.Git.Types (Commit (..)) import Data.Graph.Inductive.Graph import Data.Graph.Inductive.PatriciaTree (Gr) import Data.Graph.Inductive.Query.BFS (leveln) import Data.Hashable (Hashable (..)) import Data.HashMap.Lazy (HashMap) import Data.Maybe (catMaybes, fromMaybe) import Data.Ord (Down (..)) --import qualified Data.DList as D import qualified Data.HashMap.Lazy as M import qualified Data.Set as S import Data.Git.Graph.Util -- | Each node in the commit graph represents a commit. type NodeLabel = (ObjId, Commit) -- | Edges are tagged by numbers defining the order of parents of a commit. For -- each commit, the out-edges pointing to its parents are numbered according to -- the order in which the parents were specified in the 'commitParents' field. -- -- The 'Down' wrapper reverses the comparison (the 'Ord' instance), so that -- merged-from branches are inserted earlier into the sorted list than -- merged-to branches. type EdgeLabel = Down Int type CommitGraph g = g NodeLabel EdgeLabel -- | Build a directed acyclic graph of commits. The commits in the graph are -- the ones specified, and all their ancestors. The edges point from a commit -- to its parents. loadCommitGraph :: Graph g => Git -> [ObjId] -> IO (CommitGraph g) loadCommitGraph git refs = do let visit (_rChild, _cChild) rParent v@(nextNode, commits) = if rParent `M.member` commits then return (v, Nothing) else do cParent <- getCommit git $ unObjId rParent let commits' = M.insert rParent (cParent, nextNode) commits return ((nextNode + 1, commits'), Just cParent) cmts <- traverse (getCommit git . unObjId) refs let pairs = zip refs $ map Just cmts firstNode = 1 (next, commits) <- loadCommitsMulti git visit (firstNode, M.empty) pairs let sources = zip3 refs cmts [next..] alter cmt node Nothing = Just (cmt, node) alter _ _ j@(Just _) = j f cs (ref, cmt, node) = M.alter (alter cmt node) ref cs commits' = foldl' f commits sources nodeOf r = maybe (error "ref has no node") snd $ M.lookup r commits' mkNode l r (c, n) = (n, (r, c)) : l nodes = M.foldlWithKey' mkNode [] commits' mkEdge n l (r, e) = (n, nodeOf r, e) : l edgeNums = map Down [1..] parents = map ObjId . commitParents mkEdges l (c, n) = foldl' (mkEdge n) l $ zip (parents c) edgeNums edges = M.foldl' mkEdges [] commits' return $ mkGraph nodes edges loadCommitGraphPT :: Git -> [ObjId] -> IO (CommitGraph Gr) loadCommitGraphPT = loadCommitGraph type Depth = Int -- | Determine the depths of all the commits in the graph. Orphan commits (i.e. -- which don't have parents are assigned depth 1, and any other commit gets a -- higher depth. A commit's depth depends on the length of the shortest path -- between that commit and any of the orphan commits. commitDepths :: Graph g => CommitGraph g -> [(Node, Depth)] commitDepths g = let orphans = filter ((== 0) . indeg g) $ nodes g in leveln (zip orphans (repeat 1)) g getDepth' :: HashMap Node Depth -> Node -> Depth getDepth' depths node = case M.lookup node depths of Nothing -> error "node not found in depth map" Just d -> d getLabel' :: Graph g => g a b -> Node -> a getLabel' g n = case lab g n of Nothing -> error "node not found in graph" Just l -> l parentRefs :: Graph g => CommitGraph g -> Node -> [ObjId] parentRefs g n = map ObjId $ commitParents $ snd $ getLabel' g n -- | Return a subgraph containing only commits whose depth is up to the depth -- specified. filterDepth :: DynGraph g => Depth -> CommitGraph g -> CommitGraph g filterDepth dmax g = subgraph [n | (n, d) <- commitDepths g, d <= dmax] g -- | Given a depth threshold /D/, the commits in the graph can be partitioned -- into 3 groups: -- -- (1) Commits at depth /D/ or below, whose parents (if any) are all present in -- the graph and as well at depth /D/ or below -- (2) Commits at depth /D/ which have parents, and the parents are at depth -- /D+1/ or missing from the graph -- (3) Commits at depth /D+1/ or above -- -- In this library, these groups are called /healthy/, /shallow/ and /excluded/ -- respectively. partitionDepth :: Graph g => CommitGraph g -> HashMap Node Depth -> Depth -> ([LNode NodeLabel], [LNode NodeLabel], [LNode NodeLabel]) partitionDepth g depths thresh = let getDepth = getDepth' depths f (healthy, shallow, excluded) l@(n, (_r, c)) = let d = getDepth n in if d > thresh then (healthy, shallow, l : excluded) else let parentsN = suc g n parentsC = commitParents c allHere = length parentsN == length parentsC inThresh p = getDepth p <= thresh in if allHere && all inThresh parentsN then (l : healthy , shallow , excluded) else (healthy , l : shallow , excluded) in foldl f ([], [], []) $ labNodes g -- | Determine whether a given commit is healthy (see 'partitionDepth'). isHealthy :: Graph g => CommitGraph g -> HashMap Node Depth -> Depth -> Node -> Bool isHealthy g depths thresh node = let inThresh n = getDepth' depths n <= thresh parents = suc g node in inThresh node && length parents == length (parentRefs g node) && all inThresh parents -- | Determine whether a given commit is shallow (see 'partitionDepth'). isShallow :: Graph g => CommitGraph g -> HashMap Node Depth -> Depth -> Node -> Bool isShallow g depths thresh node = let inThresh n = getDepth' depths n <= thresh parents = suc g node in inThresh node && not ( length parents == length (parentRefs g node) && all inThresh parents ) -- | Determine whether a given commit is excluded (see 'partitionDepth'). isExcluded :: Graph g => CommitGraph g -> HashMap Node Depth -> Depth -> Node -> Bool isExcluded _g depths thresh node = getDepth' depths node > thresh -- / Like 'loadCommitGraphByRef', but lets you specify a named ref, such as a -- branch or tag name. Returns 'Nothing' if ref isn't found. {-loadCommitGraphByNameMaybe :: Graph g => Git -> String -> IO (Maybe (CommitGraph g)) loadCommitGraphByNameMaybe git name = do mref <- resolveNameMaybe git name case mref of Nothing -> return Nothing Just ref -> Just <$> loadCommitGraphByRef git ref -} -- / Like 'loadCommitGraphByNameMaybe', but throws an exception if the ref name -- can't be resolved. {-loadCommitGraphByName :: Graph g => Git -> String -> IO (CommitGraph g) loadCommitGraphByName git name = do mg <- loadCommitGraphByNameMaybe git name case mg of Nothing -> error "no such ref" Just g -> return g -} -- / Load a commit graph and topsort the commits. The resulting list starts -- with the last commit in the repo and ends with the initial commit. {-loadCommitsTopsort :: (ResultList l, Functor l) => Git -> String -> IO (l (Ref, Commit)) loadCommitsTopsort git name = do let load :: IO (CommitGraph Gr) load = loadCommitGraphByName git name graph <- load let mnodes = topsortUnmixOrder graph (NodeStack [rootN]) nodes = case mnodes of Nothing -> error "commit graph contains a cycle" Just ns -> ns return $ fmap (nodeLabel graph) nodes -} {-instance ResultList D.DList where emptyList = D.empty appendItem = flip D.snoc -} -- / Runs 'loadCommitsTopsort' with a 'D.DList', then converts to list and -- returns it. At least at the time of writing, DList mapping and folding goes -- through a regular list anyway. {-loadCommitsTopsortList :: Git -> String -> IO [(Ref, Commit)] loadCommitsTopsortList git name = D.toList <$> loadCommitsTopsort git name -}