% Copyright (C) 2003-2004 David Roundy % % 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. \section{Dependencies} \begin{code} module Depends ( get_common_and_uncommon, get_tags_right, get_common_and_uncommon_or_missing, optimize_patchset, deep_optimize_patchset, slightly_optimize_patchset, get_patches_beyond_tag, get_patches_in_tag, is_tag, patchset_union, patchset_intersection, commute_to_end, ) where import List ( delete, intersect ) import Monad ( liftM, liftM2 ) import Control.Monad.Error (Error(..), MonadError(..)) import Patch ( Patch, getdeps, join_patches, flatten, commute, patch2patchinfo, merge ) import PatchInfo ( PatchInfo, just_name, human_friendly ) import PatchSet ( PatchSet ) import Printer ( errorDoc, ($$), text ) #include "impossible.h" \end{code} \begin{code} get_tags_right :: PatchSet -> [PatchInfo] get_common_and_uncommon :: (PatchSet,PatchSet) -> ([PatchInfo],PatchSet,PatchSet) get_common_and_uncommon_or_missing :: (PatchSet,PatchSet) -> Either PatchInfo ([PatchInfo],PatchSet,PatchSet) \end{code} \begin{code} get_common_and_uncommon = either missingPatchError id . get_common_and_uncommon_err get_common_and_uncommon_or_missing = either (\(MissingPatch x) -> Left x) Right . get_common_and_uncommon_err get_common_and_uncommon_err :: (PatchSet,PatchSet) -> Either MissingPatch ([PatchInfo],PatchSet,PatchSet) get_common_and_uncommon_err (ps1,ps2) = gcau (optimize_patchset ps1) ps2 {- with_partial_intersection takes two PatchSets and splits them into a common intersection portion and two sets of patches. The intersection, however, is only lazily determined, so there is no guarantee that all intersecting patches will be included in the intersection PatchSet. This is a pretty efficient function, because it makes use of the already-broken-up nature of PatchSets. PatchSets have the property that if (fst $ last $ head a) == (fst $ last $ head b) then (tail a) and (tail b) are identical repositories, and we want to take advantage of this if possible, to avoid reading too many inventories. In the case of --partial repositories or patch bundles, it is crucial that we don't need to read the whole history, since it isn't available. TODO: The length equalising isn't necessarily right. We probably also be thinking about not going past the end of a partial repository, or favour local repository stuff over remote repository stuff. Also, when comparing l1 to l2, we should really be comparing the newly discovered one to /all/ the lasts in the other patch set that we've got so far. -} with_partial_intersection :: PatchSet -> PatchSet -> (PatchSet -> [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] -> a) -> a with_partial_intersection [] ps2 j = j [[]] [] (concat ps2) with_partial_intersection ps1 [] j = j [[]] (concat ps1) [] with_partial_intersection ([]:ps1) ps2 j = with_partial_intersection ps1 ps2 j with_partial_intersection ps1 ([]:ps2) j = with_partial_intersection ps1 ps2 j -- NOTE: symmetry is broken here, so we want the PatchSet with more history -- first! with_partial_intersection ([(pi1,_)]:common) ([(pi2,_)]:_) j | pi1 == pi2 = j common [] [] with_partial_intersection (orig_ps1:orig_ps1s) (orig_ps2:orig_ps2s) j = f (length orig_ps1) (fst $ last orig_ps1) [orig_ps1] orig_ps1s (length orig_ps2) (fst $ last orig_ps2) [orig_ps2] orig_ps2s where {- Invariants: nx = length $ cr psx lx = last $ cr psx -} f _n1 l1 ps1 ps1s _n2 l2 ps2 _ps2s | l1 == l2 = j ps1s (cr ps1) (cr ps2) f n1 l1 ps1 ps1s n2 l2 ps2 ps2s = case compare n1 n2 of GT -> case dropWhile null ps2s of ps2':ps2s' -> f n1 l1 ps1 ps1s (n2 + length ps2') (fst $ last ps2') (ps2':ps2) ps2s' [] -> -- We keep going round f so the l1 == l2 case -- has a chance to kick in case dropWhile null ps1s of ps1':ps1s' -> f (n1 + length ps1') (fst $ last ps1') (ps1':ps1) ps1s' n2 l2 ps2 ps2s [] -> j [[]] (cr ps1) (cr ps2) _ -> case dropWhile null ps1s of ps1':ps1s' -> f (n1 + length ps1') (fst $ last ps1') (ps1':ps1) ps1s' n2 l2 ps2 ps2s [] -> -- We keep going round f so the l1 == l2 case -- has a chance to kick in case dropWhile null ps2s of ps2':ps2s' -> f n1 l1 ps1 [] (n2 + length ps2') (fst $ last ps2') (ps2':ps2) ps2s' [] -> j [[]] (cr ps1) (cr ps2) cr = concat . reverse {- gcau determines a list of "common" patches and patches unique to each of the two PatchSets. The list of "common" patches only needs to include all patches that are not interspersed with the "unique" patches, but including more patches in the list of "common" patches doesn't really hurt, except for efficiency considerations. Mostly, we want to access as few elements as possible of the PatchSet list, since those can be expensive (or unavailable). PatchSets have the property that if (fst $ last $ head a) == (fst $ last $ head b) then (tail a) and (tail b) are identical repositories, and we want to take advantage of this if possible, to avoid reading too many inventories. In the case of --partial repositories or patch bundles, it is crucial that we don't need to read the whole history, since it isn't available. TODO: The length equalising isn't necessarily right. We probably also be thinking about not going past the end of a partial repository, or favour local repository stuff over remote repo stuff. Also, when comparing l1 to l2, we should really be comparing the newly discovered one to /all/ the lasts in the other patch set that we've got so far. -} gcau :: PatchSet -> PatchSet -> Either MissingPatch ([PatchInfo],PatchSet,PatchSet) gcau [] ps2 = return ([], [[]], [concat ps2]) gcau ps1 [] = return ([], [concat ps1], [[]]) gcau ([]:ps1) ps2 = gcau ps1 ps2 gcau ps1 ([]:ps2) = gcau ps1 ps2 gcau ([(pi1,_)]:_) ([(pi2,_)]:_) | pi1 == pi2 = return ([pi1], [[]], [[]]) gcau (orig_ps1:orig_ps1s) (orig_ps2:orig_ps2s) = f (length orig_ps1) (fst $ last orig_ps1) [orig_ps1] orig_ps1s (length orig_ps2) (fst $ last orig_ps2) [orig_ps2] orig_ps2s where {- Invariants: nx = length $ cr psx lx = last $ cr psx -} f _n1 l1 ps1 _ps1s _n2 l2 ps2 _ps2s | l1 == l2 = gcau_simple (cr ps1) (cr ps2) f n1 l1 ps1 ps1s n2 l2 ps2 ps2s = case compare n1 n2 of GT -> case dropWhile null ps2s of ps2':ps2s' -> f n1 l1 ps1 ps1s (n2 + length ps2') (fst $ last ps2') (ps2':ps2) ps2s' [] -> -- We keep going round f so the l1 == l2 case -- has a chance to kick in case dropWhile null ps1s of ps1':ps1s' -> f (n1 + length ps1') (fst $ last ps1') (ps1':ps1) ps1s' n2 l2 ps2 ps2s [] -> gcau_simple (cr ps1) (cr ps2) _ -> case dropWhile null ps1s of ps1':ps1s' -> f (n1 + length ps1') (fst $ last ps1') (ps1':ps1) ps1s' n2 l2 ps2 ps2s [] -> -- We keep going round f so the l1 == l2 case -- has a chance to kick in case dropWhile null ps2s of ps2':ps2s' -> f n1 l1 ps1 [] (n2 + length ps2') (fst $ last ps2') (ps2':ps2) ps2s' [] -> gcau_simple (cr ps1) (cr ps2) cr = concat . reverse gcau_simple :: [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] -> Either MissingPatch ([PatchInfo],PatchSet,PatchSet) gcau_simple ps1 ps2 = do ex1 <- get_extra (return []) common ps1 ex2 <- get_extra (return []) common ps2 let ps1' = filter ((`elem` common) . fst) ps1 return (map fst $ head $ ((optimize_patchset [ps1']) ++ [[]]) , [ex1] , [ex2]) where common = map fst ps1 `intersect` map fst ps2 newtype MissingPatch = MissingPatch PatchInfo instance Error MissingPatch where -- we don't really need those methods noMsg = MissingPatch (error "MissingPatch: bug in get_extra (noMsg)") strMsg msg = MissingPatch (error ("MissingPatch: " ++ msg)) get_extra :: Either MissingPatch [Patch] -> [PatchInfo] -> [(PatchInfo, Maybe Patch)] -> Either MissingPatch [(PatchInfo, Maybe Patch)] get_extra _ _ [] = return [] get_extra skipped common ((pinfo, mp):pps) = if pinfo `elem` common && is_tag pinfo then case liftM getdeps mp of Just ds -> get_extra (liftM2 (:) ep skipped) (ds++delete pinfo common) pps Nothing -> get_extra (liftM2 (:) ep skipped) (delete pinfo common) pps else if pinfo `elem` common then get_extra (liftM2 (:) ep skipped) (delete pinfo common) pps else do p <- ep skpd <- skipped case commute (join_patches skpd, p) of Just (p', skipped_patch') -> do x <- get_extra (return (flatten skipped_patch')) common pps return ((pinfo, Just p') : x) Nothing -> errorDoc $ text "bug in get_extra commuting patch:" $$ human_friendly pinfo where ep = case mp of Just p' -> return p' Nothing -> throwError (MissingPatch pinfo) missingPatchError :: MissingPatch -> a missingPatchError (MissingPatch pinfo) = errorDoc ( text "failed to read patch in get_extra:" $$ human_friendly pinfo $$ text "Perhaps this is a 'partial' repository?" ) get_extra_old :: [Patch] -> [PatchInfo] -> [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] get_extra_old skipped common pps = either missingPatchError id (get_extra (return skipped) common pps) \end{code} \begin{code} get_patches_beyond_tag :: PatchInfo -> PatchSet -> PatchSet get_patches_beyond_tag t ([(pinfo,_)]:_) | pinfo == t = [[]] get_patches_beyond_tag t patchset@(((pinfo,mp):ps):pps) = if pinfo == t then if get_tags_right patchset == [pinfo] then [[]] -- special case to avoid looking at redundant patches else [get_extra_old [] [t] $ concat patchset] else (pinfo,mp) -:- get_patches_beyond_tag t (ps:pps) get_patches_beyond_tag t ([]:pps) = get_patches_beyond_tag t pps get_patches_beyond_tag _ [] = [[]] get_patches_in_tag :: PatchInfo -> PatchSet -> PatchSet get_patches_in_tag t pps@([(pinfo,_)]:xs) | pinfo == t = pps | otherwise = get_patches_in_tag t xs get_patches_in_tag t (((pinfo,_):ps):xs) | pinfo /= t = get_patches_in_tag t (ps:xs) get_patches_in_tag _ ((pa@(_, Just tp):ps):xs) = gpit thepis [pa] (ps:xs) where thepis = getdeps tp gpit _ sofar [] = [reverse sofar] gpit deps sofar ([(tinfo,thisp)]:xs') | tinfo `elem` deps = (reverse $ (tinfo,thisp) : sofar) : xs' | otherwise = gpit deps sofar xs' gpit deps sofar ([]:xs') = gpit deps sofar xs' gpit deps sofar (((pinf, Just p):ps'):xs') | pinf `elem` deps = let odeps = filter (/=pinf) deps alldeps = if is_tag pinf then odeps ++ getdeps p else odeps in gpit alldeps ((pinf, Just p):sofar) (ps':xs') | otherwise = gpit deps (commute_by sofar p) (ps':xs') gpit _ _ (((pinf, Nothing):_):_) = errorDoc $ text "Failure reading patch file" $$ human_friendly pinf get_patches_in_tag t _ = errorDoc $ text "Couldn't read tag" $$ human_friendly t commute_by :: [(PatchInfo, Maybe Patch)] -> Patch -> [(PatchInfo, Maybe Patch)] commute_by [] _ = [] commute_by ((pinf, Just a):xs) p = case commute (a,p) of Nothing -> bug "Failure commuting patches in commute_by called by gpit!" Just (p', a') -> (pinf, Just a') : commute_by xs p' commute_by ((pinf, Nothing):_) _ = errorDoc $ text "Couldn't read patch:" $$ human_friendly pinf \end{code} \begin{code} is_tag :: PatchInfo -> Bool is_tag pinfo = take 4 (just_name pinfo) == "TAG " get_tags_right [] = [] get_tags_right (ps:_) = get_tags_r ps where get_tags_r [] = [] get_tags_r ((pinfo,mp):pps) | is_tag pinfo = case liftM getdeps mp of Just ds -> pinfo : get_tags_r (drop_tags_r ds pps) Nothing -> pinfo : map fst pps | otherwise = pinfo : get_tags_r pps drop_tags_r :: [PatchInfo] -> [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] drop_tags_r [] pps = pps drop_tags_r _ [] = [] drop_tags_r ds ((pinfo,mp):pps) | pinfo `elem` ds && is_tag pinfo = case liftM getdeps mp of Just ds' -> drop_tags_r (ds'++delete pinfo ds) pps Nothing -> drop_tags_r (delete pinfo ds) pps | pinfo `elem` ds = drop_tags_r (delete pinfo ds) pps | otherwise = (pinfo,mp) : drop_tags_r ds pps \end{code} \begin{code} deep_optimize_patchset :: PatchSet -> PatchSet deep_optimize_patchset pss = optimize_patchset [concat pss] optimize_patchset :: PatchSet -> PatchSet optimize_patchset [] = [] optimize_patchset (ps:pss) = opsp ps ++ pss opsp :: [(PatchInfo,Maybe Patch)] -> PatchSet opsp [] = [] opsp ((pinfo,mp):pps) | is_tag pinfo && get_tags_right [(pinfo,mp):pps] == [pinfo] = [(pinfo,mp)] : opsp pps | otherwise = (pinfo,mp) -:- opsp pps (-:-) :: (PatchInfo, Maybe Patch) -> PatchSet -> PatchSet pp -:- [] = [[pp]] pp -:- (p:ps) = ((pp:p) : ps) slightly_optimize_patchset :: PatchSet -> PatchSet slightly_optimize_patchset [] = [] slightly_optimize_patchset (ps:pss) = sops ps ++ pss where sops [] = [] sops [(pinfo,mp)] = [[(pinfo,mp)]] sops ((pinfo,mp):pps) | is_tag pinfo && get_tags_right [(pinfo,mp):pps] == [pinfo] = [(pinfo,mp)] : [pps] | otherwise = (pinfo,mp) -:- sops pps \end{code} \begin{code} commute_to_end :: [Patch] -> PatchSet -> ([Patch],[Patch]) commute_to_end select from = ctt [] (map (fromJust.patch2patchinfo) select) (concat from) where ctt :: [Patch] -> [PatchInfo] -> [(PatchInfo, Maybe Patch)] -> ([Patch], [Patch]) ctt skp [] _ = ([],skp) ctt skp sel ((pinf, Just p):ps) | pinf `elem` sel = case cmt_by (skp, p) of Nothing -> bug "patches to commute_to_end does not commute (1)" Just (p', skp') -> let (ps', skp'') = ctt skp' (delete pinf sel) ps in (p':ps', skp'') | otherwise = ctt (p:skp) sel ps ctt _ _ _ = bug "patches to commute_to_end does not commute (2)" cmt_by :: ([Patch], Patch) -> Maybe (Patch, [Patch]) cmt_by ([], a) = Just (a, []) cmt_by (p:ps, a) = case commute (p, a) of Nothing -> Nothing Just (a', p') -> case cmt_by (ps, a') of Nothing -> Nothing Just (a'', ps') -> Just (a'', p':ps') segregate_patches :: (PatchInfo -> Bool) -> PatchSet -> ([Patch],[Patch]) segregate_patches select_first from = ctt [] [] (concat from) where ctt :: [Patch] -> [Patch] -> [(PatchInfo, Maybe Patch)] -> ([Patch], [Patch]) ctt las fir [] = (reverse las, reverse fir) ctt las fir ((pinf, Just p):ps) | select_first pinf = ctt las (p:fir) ps | otherwise = case cmt_by (fir, p) of Nothing -> bug "patches to segregate_patches does not commute (1)" Just (p', fir') -> ctt (p':las) fir' ps ctt _ _ ((pinf, Nothing):_) = errorDoc ( text "failed to read patch in get_extra:" $$ human_friendly pinf $$ text "Perhaps this is a 'partial' repository?" ) cmt_by :: ([Patch], Patch) -> Maybe (Patch, [Patch]) cmt_by (ps, a) = do (a', jps') <- commute (join_patches ps, a) return (a', flatten jps') \end{code} \begin{code} patchset_intersection :: [PatchSet] -> PatchSet patchset_intersection [] = [[]] patchset_intersection [x] = x patchset_intersection (y:ys) = with_partial_intersection y (patchset_intersection ys) $ \common a b -> let morecommon = map fst a `intersect` map fst b (_,commonps) = segregate_patches (`elem` morecommon) [a] in (map (\p -> (fromJust $ patch2patchinfo p, Just p)) commonps) : common patchset_union :: [PatchSet] -> PatchSet patchset_union [] = [[]] patchset_union [x] = x patchset_union (y:ys) = with_partial_intersection y (patchset_union ys) $ \common a b -> case gcau_simple a b of Left e -> missingPatchError e Right (_, [a'], [b']) -> (merge_sets a' b' ++ b) : common _ -> impossible merge_sets :: [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] -> [(PatchInfo, Maybe Patch)] merge_sets l r = let pl = join_patches $ map (fromJust . snd) $ reverse l pr = join_patches $ map (fromJust . snd) $ reverse r p2pimp p = (fromJust $ patch2patchinfo p, Just p) in case merge (pl, pr) of Just (pl',_) -> map p2pimp $ reverse $ flatten pl' Nothing -> impossible \end{code}